Quantcast
Channel: Rubyの記事一覧|TechRacho by BPS株式会社
Viewing all 1100 articles
Browse latest View live

はじめての正規表現とベストプラクティス3: 冒頭/末尾にマッチするメタ文字とセキュリティ、文字セットの否定と範囲

$
0
0

更新情報
– 2018/11/07: 初版公開
– 2021/01/22: 細部を更新

⚓正規表現はじめの九歩: \A\z

\A
文字列の冒頭を表す(\A大文字!)
\z
文字列の末尾を表す(\z小文字!)

\A\zは、対象となる文字列全体(改行も含む)の冒頭や末尾を表します。以下の2つの例は、どちらも改行を含む文字列の冒頭または末尾に接している言葉だけにマッチしています。

\A\zがマッチするのは文字列そのものではないことにご注意ください。また、パターンの途中に置く意味はありません。

  • 例: /\Aにわ/というパターン(Rubular

  • 例: /もも\z/というパターン(Rubular

注: \A\zのように、\で始まるメタ文字(文字列アンカー)のサポートはライブラリごとの違いが割とあります。たとえばPOSIX BREPOSIX EREでは原則としてサポート外です(言い切れないのが歯がゆいところですが)。
本記事では、RubyやPythonやPerlやPHPといった比較的高機能な正規表現ライブラリを前提としています。

⚓\A\zと、|の合わせ技

第1回で学んだ|は、\A\zよりも優先順位が低いので、次のように|で区切ったパターンそれぞれに\A\zを書くこともできます。

  • 例: /\Aすも|いる\z/というパターン(Rubular

|でつないでいるので、片方だけのマッチでも両方のマッチでも、マッチとして扱われます。

以下の例では、対象文字列の2行目を削除すれば1行目末尾の「もも」にマッチします。Rubularで試してみましょう。

  • 例: /もも\z|いる\z/というパターン(Rubular

⚓⚠警告: 冒頭や末尾を^$で表すときはセキュリティに注意

^
行の冒頭を表す(原則避ける)
$
行の末尾を表す(原則避ける)

一般の正規表現の資料では、冒頭や末尾を表すメタ文字として真っ先に^$が記載されています。そしてネットに落ちている正規表現でも^$が多用されています。

ただし少なくともRubyやPHP、PCRE(Perl)、またはPCREっぽい正規表現ライブラリでは、^$というメタ文字をユーザー入力で使うと、多くの場合脆弱性の元になる場合があります

^$で脆弱性が発生するかどうかは、正規表現に与えるオプションにもよります。たとえばPHPではmを指定しなければ^$の挙動は\A\zと同じになります。
参考: PHP: 正規表現パターンに使用可能な修飾子 - Manual
本シリーズでは、正規表現の外から与えるオプションについては原則扱いません。

次の例をご覧ください。「すも」で始まり「もも」で終わる文字列だけを使おうとして以下の正規表現を書いたために、Set-Cookie: SESSIONID=ABCというコードを注入されています。つまり、マッチしてはいけないはずの文字列にマッチしてしまっています。この方法でSQL文を注入されれば、いわゆるSQLインジェクションという致命的な脆弱性につながります。

例: /^すも.+?もも$/というパターンで注入が発生(Rubular

^$を原則として避けるべき理由は、上記のライブラリでは「文字列の冒頭や末尾」ではなく「行の冒頭や末尾」にマッチするためです。

少なくとも、Webアプリの入力値のような「信頼できない文字列」のバリデーションでは^$で冒頭や末尾を表すことは避けましょう。以下のRubyスタイルガイドにも同様の記載があります。

参考: Rubyスタイルガイドを読む: 正規表現

⚓^$のまっとうな使用例

もちろん、以下の「改行を含めた正規表現検索」のように、^$を使わないと表せない正規表現もあります。何が何でも使ってはいけないというものではありませんが、それでも十分慎重に扱うべきです。^$をあえて本記事の「はじめのn歩」に含めなかったのはこれが理由です。

以下はいずれも「信頼できる文字列」を対象として仮定しています。

  • 例: /^すも/で複数行の文字列で行の冒頭文字とマッチさせる(Rubular

  • 例: /すも\nもも$/というパターン(Rubular

なお、上は/すも\n^もも$/のように^を追加しても同じようにマッチしますが、冗長です。

⚓正規表現はじめの十歩: [^文字]による文字セットの否定表現

[^文字]
指定された文字でない任意の1文字(否定)

はじめの六歩で学んだ文字セット[ ]の冒頭に^を置くと、文字セットの否定を表現できます。

これは先ほどの^とは異なり、使っても大丈夫な^です。

[^文字]も文字セットなので、文字セット全体は1文字として扱われる点にご注意ください。たとえば[^ABC]という文字セットは、「AでもBでもCでもない1文字」という意味です。よく間違えられるのですが、「ABCではない文字列」ではありませんので、単語やフレーズの否定は1つの文字セットで表せません。

  • 例: /[^青赤]巻紙/というパターン(Rubular

もうひとつありがちな勘違い: /[^青赤]巻紙/は、「巻紙の前が青や赤ではない」ではありません。「巻紙の前は、青でも赤でもない1文字」です。文字セット[]はあくまで1文字であることを思い出しましょう。

なお、$[ ]の中では単なる文字でしかなく、機能はありません。詳しくは文字クラス [ ] 内でエスケープしなくてもよい記号をご覧ください。

⚓⚠注意: 否定文字セットは改行文字にもマッチする

ひとつ注意すべきは、たとえば[^xyz]という否定文字セットは、\n\r\tといった改行文字や非印刷文字にもマッチするという点です(.が改行文字にマッチしないのと対照的です)。

  • 例: /[^青赤]巻紙/というパターンで想定外のマッチが発生(Rubular

たとえば/[^xyz\n\r\t]/などとしないと、知らずに改行とマッチしてしまう事故につながります。これは自分もやらかしたことがあります💦

⚓正規表現はじめの十一歩: [ - ]による文字範囲表現

[a-z]
aからzまでのいずれかの1文字を表す
[a-z0-9]
aからz0から9までのいずれかの1文字を表す

文字セット[ ]の中で2つの文字を-でつなぐと、文字セットの文字の範囲を簡潔に表せます。これも非常に有用な書式です。-は文字セットの中でのみ常に機能を持つメタ文字です。

たとえば、ASCII数字1文字を表す[0123456789]という文字セットは、[0-9]と表せます。

上の[a-z0-9]のように、文字セット[ ]の中で複数の範囲も記述できます。範囲はいくつでも追加できます。

なお、-そのものを文字セット[ ]の中で使いたい場合は、主に以下の2とおりの方法があります。

  • -[ ]の中で最後に置く(苦し紛れ感)
    • 例: /[a-zA-Z0-9_?$%#@-]/
  • バックスラッシュ\でエスケープする
    • 例: /[\-]/

なお、-で表す文字単位は文字セット内で連続していなくても構いません。たとえば以下の2つ目は1つ目の記号をわざと文字範囲の隙間に移動したものですが、結果は同じですので、Rubularで開いて試してみてください。

  1. /[a-zA-Z0-9_?$%#@-]/(Rubular
  2. /[a-z_$?%A-Z#@0-9-]/(Rubular

しかし2.のような混在した書き方は混乱を招くだけなので、スタイルとして範囲は文字セットの前半にまとめて置くようにしましょう。未来の自分のためにも。

  • 例: /[0-5]ピョコピョコ/というパターン(Rubular

⚓⚠注意: 文字範囲[ - ]を雑に指定しないこと

たとえば横着して[a-zA-Z]という文字範囲を[A-z]などと書くと、[_などといった記号にまでマッチしてしまいます。以下の文字コード表を見れば理由はおわかりかと思います(Unicodeの文字コード表ですが、この部分の並びはASCIIと同じです)。私は[A-z]という書き方自体使おうなどと思いつきませんでした。

文字範囲を使う前には文字コード表などで余分な文字を巻き込んでいないかどうかチェックする癖を付けましょう。くれぐれも雰囲気で範囲を指定しないように。文字セット[ - ]を精密に記述しようとすると、しばしば文字をべたに列挙するしかないこともあります。

当然ですが、文字セット範囲[-]は文字コード表順にチェックされるので、[a-Z]のように逆順に書いても無効です((Rubular))。

詳しくは以下の参考記事でも説明されています。

参考: 正規表現を使うときに注意すべきこと - Qiita

⚓保存版: よく使われる文字範囲

以下はよく使われる文字セット範囲のイディオムを私なりに吟味したものですが、コピペする前によくお読みください。すべて日本語のみを想定しています。

数字
[0-9]
英字(大文字小文字)
[a-zA-Z]
英数字
[a-zA-Z0-9]
英数字と半角スペースとASCII記号
[a-zA-Z0-9 !"#$%&'()*,.\/:;<>?@\[\\\]\^_`{|}~-]
-を最後に置いているのがポイント
⚠エスケープはRuby用になっているので他のライブラリでは適宜エスケープを調整すること
(まあ実用的な)全角ひらがな
[ぁ-ん]
⚠などにはマッチするがといった特殊なひらがなにはマッチしない(Unicode Hiragana block
(まあ実用的な)全角カタカナ
[ァ-ヴー]
⚠長音も入れないと不完全だがひらがなの後ろの長音にもマッチしてしまう
⚠などはマッチするがといった特殊なカタカナにはマッチしない(Unicode Katakana block
(もう少し実用的な)全角カタカナ語
[アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン][ァ-ヴー]+?
⚠カタカナの冒頭に来ない文字を除外
(まあ実用的な)Unicode漢字
[一-龠]
・レアな中国語の漢字は漏れる
⚠よくネットに落ちている[亜-熙]はShift-JIS用の古い書き方
(まあ実用的な)Unicode漢数字
[〇一二三四五六七八九十百千万億兆京]
・べた書きのみ
(非実用)Unicode漢数字(大字含む)
[零〇壱一弐二参三肆四伍五陸六漆七捌八玖九拾十陌百阡千萬万億兆京]
・べた書きのみ

⚓おまけ: .NET Frameworkの「文字クラスの減算」

少し先走りますが、.NET Frameworkの「文字クラスの減算」について簡単に説明します。

文字セットの中で使う文字クラス(今後説明します)を減算する機能は、.NET Frameworkならではの強力な機能です(つまり方言)。次の書式が使えます。

[基本グループ-[除外グループ]]

これを使うと、たとえば「半濁点を含まないひらがな」という文字セットを次のように簡潔に表現できます。

  • 例: /[\p{IsHiragana}-[ぱぴぷぺぽ]]/というパターン(regexstorm.net

正直、この機能がうらやましくて仕方ありません。文字セットの減算を使えるのは、今のところ.NET Frameworkの他にはごくわずかです。

追記

RubyやPerlでは文字セットの共通集合(intersect)を用いて減算できることがわかりました。

Rubyの正規表現は`&&`のintersectで文字セットの「差分」を取れる

⚓正規表現は「フレーズの否定」が苦手

このシリーズ記事の#3で初めて否定を取り上げたのは、否定表現がしばしば正規表現でつまづきの元になるためです。

正規表現を書いてると、時たま猛烈に欲しくなるのが「AAAやBBBやCCCのどれでもない文字列」というフレーズの否定表現です。

しかしそれを簡単に実現できる手段は正規表現にありません。理由は、フレーズの否定をまともに実装しようとすると効率が悪くなることが予想されるためです。

否定の文字セット[^]を組み合わせれば実現できるのではないかという気がしてきますが(実際、原理的には可能ですが)、多くの場合徒労に終わります

仮に「ABCDという文字列を含まない正規表現」を無理やり既存の方法で書くとたとえばこうなります(引用元)↓。こういうフレーズの否定表現を自動生成するスクリプトがネットに落ちていたりします。

^([^A]+|A+(BC?A+)*([^BA]|B([^CA]|C([^DA]|$)|$)|$))*$

ご覧のとおり可読性が著しく落ち、後で自分が読んでもきっとわけがわからなくなります。フレーズの否定は正規表現で頑張るより、正規表現を使うコード側で工夫しましょう。

次回説明する「先読み」「後読み」を使えば擬似的にフレーズの否定を表現できることもありますが、個人的にはあまりおすすめしません。

⚓grepコマンドの-vは覚えておこう

たとえばLinuxコマンドのgrepであれば、grep -vオプションを使うことで「指定の正規表現にマッチしない行」だけを簡単に取り出せます。

cat text.txt | grep -v "ABCD"

正規表現そのもので頑張るより、この発想で取り組む方が話が早いと思います。

(おまけ)実はRuby 2.4.1〜にはフレーズ否定の正規表現がある!

詳しくは以下の記事をどうぞ。

Ruby 2.4.1新機能: Onigmo正規表現の非包含演算子(?~ )をチェック


関連記事

Rubyスタイルガイドを読む: 正規表現、%リテラル、メタプログラミング(最終回)

正規表現: 文字クラス [ ] 内でエスケープしなくてもよい記号


Rubyの整数リテラルの単項演算子を再現する正規表現を書いてみた

$
0
0

はじめに: Rubyの数値リテラルの単項演算子

Rubyのリテラルのうち、少なくとも整数リテラルには、以下のように符号を複数持つ単項演算子を付けることもできます(小数リテラルなどの数値リテラルや変数などの単項演算子については調べていません)。

# 1つの行がひとつの単項です
1
 1

-1
---1
-----1

 -1
 ---1
 -----1

- 1
--- 1
----- 1

+- 1
+--- 1
+----- 1

-+1
---+1
-----+1

- + 1
--- + 1
----- + 1

-1
-+-+-1
+- + + - -+ + + - -1

--1
----1
------1

+--+1

※Rubyの日本語ドキュメント↓を調べてみると、単項演算子を「+符号や-符号やスペース文字を0個以上含むもの」とする説明は見当たりませんでしたが、本記事では便宜上「+符号や-符号やスペース文字を0個以上含むもの」も単項演算子と呼ぶことにします。

参考: バージョン:3.0.0 > クエリ:単項演算子 | るりまサーチ (Ruby 3.0.0)


この単項演算子の挙動をRubyのirbで調べたところ、冒頭のリストの各行はいずれも単項として、たとえば以下のように計算式を含む式(以下単に計算式と呼びます)の項に追加できます。

irb(main):002:0> +- + + - -+ + + - -1 +- + + - -+ + + - -1----- + 1
=> -3

irbで調べた挙動を以下にまとめました。

  • ひとつの整数リテラルには「+符号や-符号やスペース文字を0個以上含む」単項演算子を付けられる
  • 偶数個ある-はキャンセルされる
  • 単項演算子内のスペースは無視される
  • 最終的には1個の+または-単項演算子として認識される

ご存知のように、符号なしの数値リテラルを以下の1 1のように計算式の2個目以降の単項式に書くことはできません。

irb(main):001:0> 1 1
Traceback (most recent call last):
        3: from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.0.0/bin/irb:23:in `<main>'
        2: from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.0.0/bin/irb:23:in `load'
        1: from /Users/hachi8833/.config/anyenv/envs/rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
SyntaxError ((irb):1: syntax error, unexpected integer literal, expecting end-of-input)
irb(main):002:0>

単項演算子で符号を複数書ける理由

Rubyの計算式に+- + + - -+ + + - -1のように書ける理由を考えてみました。

まずRubyの挙動として明らかだと思う点を以下にリストアップしてみます。

  • Rubyでは配列リテラルの要素やハッシュリテラルのキーおよびバリューなどは一般にカンマ「,」で区切られる
  • Rubyの式は、数値リテラルも含めて例外的に、カンマではなくスペース文字「」で区切られる

Rubyの計算式は、多くの言語と同様、学校で習うような計算式の書き方から大きくかけ離れない仕様になっていると私は認識しています。

1 + 1
1 - 1
-1 + 1
-1 - 1

だとすると、計算式を以下のようにも書きたいこともあるはずです。

1 + -1
1 - +1

さらにRubyでは、計算式の単項をスペースなしで以下のように記述することもできます。

1+-1
1-+1

このような計算式をRubyの内部で場合分けして解析するよりは、Ruby内部では整数リテラルの単項で以下のような書き方も認め、さらに「スペースを無視する」「プラス符号+やマイナス符号-はひとつだけ残す」「マイナス符号-が偶数個ある場合はキャンセルする」ことで、計算式の中の単項を統一的に扱えるようにしたのだろうと思いました。

-+1
---+1
-----+1
- + 1
--- + 1
----- + 1
-+-+-1
+- + + - -+ + + - -1

その代わり、1 1のような書き方だけは認めないということになります。

仮に1という単項を必ず+1と書かなければいけないルールにすればシンプルになりそうですが、+1 +1と書かなければならなくなってRubyユーザーにとって不便なので、普通そのようなルールにはしないだろうと思います。

以上、Rubyの挙動を元にした推測でした。

符号を複数持つ単項が最終的にマイナスかどうかを判定する正規表現

今自分が推測したRubyの単項演算子の挙動の一部を正規表現でお遊び再現してみました。なお、このRubular.comで使われているRubyのバージョンは2.5.7です。

この正規表現は、整数リテラルの単項の前に-が奇数個ある場合に、単項演算子全体にマッチします。+やスペース文字が単項の前にあっても挙動は変わりません。--1のようにキャンセルされる場合も判定します。あくまで行で区切られた単項が対象なので、単項の連続はそのままでは判定できません。


rubular.comより

この正規表現のうち、*-[ +]*の「-」を削除すると以下のようにロジックが反転し、単項がプラスになる場合にマッチします。


rubular.comより

ロジック反転に成功したとき、なぜか映画『オズの魔法使』の中盤でモノクローム映像がフルカラーに変わる瞬間を思い出してしまいました。見たこともなかったのに。

参考: オズの魔法使 - Wikipedia

雑に解説

^[+ ]*
+またはスペース文字で始まることを示す。これがないと+---1のように先頭に+があるパターンや、-が1個しかないパターンがこぼれてしまいます。*で量指定子を「0個以上」とするのがポイント。
([+ ]*-[+ ]*-[+ ]*)*
-が偶数個ある部分にマッチする。--の前後および中間にある+とスペース文字もマッチに含めます。全体を()*で囲んで、量指定子を「0個以上」とするのがポイント。
-
最終的に残る-符号。これを消すとロジックが反転します。
[ +]*
最終的な-符号の後と数値の間の+やスペースにマッチします。これも*で量指定子を「0個以上」とするのがポイント。
(?=[0-9])
後ろに数値が続くことを指定する。(?=)は「先読み」(look ahead)という正規表現によくある拡張機能です。

TechRachoの正規表現記事にも何度も書いているように、普段の私は正規表現の*メタ文字をできるだけ避けているのですが、今回の正規表現は自分としては珍しく*を連発しています。*がないと書けないはずなので別にいいんですが、軽く敗北感があります。今回は[^何とか]のような否定表現を使わずに書けただけでもよかったと思うことにします。

おまけ

Rubyの単項演算子の符号を調べているときに、たまたま以下のツイートを見かけました。

ツイートは個別の単項の符号ではなく、異符号の加法における最終的な和の求め方についての議論なので本記事に直接関係ありません。ちょうどRubyの単項演算子の符号を考えているところだったのでドキッとしたのでした。

関連記事

はじめての正規表現とベストプラクティス#1: 基本となる8つの正規表現

週刊Railsウォッチ(20210201前編)Webpackerのガイドがマージ、RailsはRuby 3でどのぐらい速くなったかほか

$
0
0

こんにちは、hachi8833です。

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsの最新情報などの記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下の公式更新情報が出ていました。


つっつきボイス:「今週は新年第2号の公式更新情報出てるんですね」「Railsリポジトリのmaster->mainブランチ変更は既にウォッチでも報じました(ウォッチ20210125)が、公式更新情報で以下のツイートにもリンクしていました↓」「今はブランチ名をmaster->mainに移行するのが世の中の流れですね」

その他は以下のコミットリストのChangelogを中心に見繕いました。

🔗 WebpackerガイドがEdge Guidesに追加される


つっつきボイス:「BPSの社内Slackに流れてた情報ですね」

「Rails Guides(英語)とEdge Guides(英語)を見てみたところ、WebpackerガイドはまだEdge Guidesにしかありませんでした↓」「それはまだmaster、じゃなくてmainブランチにしか入っていないからですね: 通常は次のリリースで正規のRails Guidesに載ることになります」「あ、そうでした」

Edge Guides: Webpacker — Ruby on Rails Guides

Rails Guides: Ruby on Rails Guides

「WebpackerのREADME↓も簡単にチェックしてみたところ、Edge GuidesのWebpackerドキュメントはこれとは違う内容でした」「必要そうなことはWebpacker Guidesにだいたい盛り込まれている感じかな👍

「Webpackerのドキュメント、ありそうでなかったのか」「そうなんです、Rails 5の頃にこんなつなぎ記事↓を書いたのを思い出しました」「Webpackerがリリースされてから随分経つのに今までなかったとはね…」「このプルリクは”いいね”が多めだったので、それだけ待ち望まれていたということでしょうね」

【保存版】Rails 5 Webpacker公式ドキュメントの歩き方+追加情報

🔗 content_typeメソッドがContent-Typeヘッダーをそのまま返すよう修正


つっつきボイス:「なるほど、変更後はcontent_typeメソッドがtext/csv; header=present; charset=utf-16"の部分を加工せずに丸ごと返してくれるようになったんですね」

参考: Content-Type – HTTP | MDN

「Rails Guides(英語版)の資料も更新されていて、デフォルトの挙動をreturn_only_request_media_type_on_content_typeコンフィグで変更できるようになっている↓」「既存のRailsアプリがcontent_typeメソッドを使っていたら、このコンフィグパラメータで以前の挙動に戻せますね」「なるほど」

# guides/source/configuring.md#1050
#### For '6.2', defaults from previous versions below and:
 - `config.action_view.button_to_generates_button_tag`: `true`
 - `config.action_view.stylesheet_media_default` : `false`
 - `config.active_support.key_generator_hash_digest_class`: `OpenSSL::Digest::SHA256`
 - `config.active_support.hash_digest_class`: `OpenSSL::Digest::SHA256`
+- `config.action_dispatch.return_only_request_media_type_on_content_type`: `false`

参考: Rails アップグレードガイド – Railsガイド

「なるほど、MIMEタイプだけ欲しい場合はActionDispatch::Request#media_typeメソッドで取れるのか」

参考: メディアタイプ – Wikipedia

「以下でmedia_typeメソッドにも修正が入ってますね↓」

🔗 コンフィグファイルで互換性を維持するRails

「この変更では、ライブラリに変更をかけるときに既存のコードに与えるインパクトをできるだけ抑えているのがいいですね: いろいろと参考になります👍」「そうですね」

「Railsは互換性をあんまり気にせずに変えるときは変えるのかなと何となく思ってましたけど、そうでもないんですね」「Railsがデフォルトの挙動を変えることはたまにありますが、そういう場合でも以前の挙動が必要な人はコンフィグで調整可能にする余地を作っておくあたりがRailsらしいと思います」「たしかに!」

「きっとこのコンフィグも、今後既存のRailsアプリでrails app:updateするときに生成されるconfig/initializers/new_framework_defaults.rbに追加されるんでしょうね↓」

参考: 1.5 フレームワークのデフォルトを設定する — Rails アップグレードガイド – Railsガイド

「その場合は古い設定のコンフィグが追加されるんでしょうか?」「Railsのこういうコンフィグは後方互換性を維持する方向で生成されるはずですし、少なくともrails app:updateすると”こういう設定があるからチェックして”みたいに知らせてくれますよ」「え、やったことなくて知らなかった」「rails app:updateは基本的に従来のデフォルトの挙動を使うコードがなるべくそのまま使えるように作られていると思います」「そうだったんですね」


ActionDispatch::Request#content_typeがContent-Typeヘッダーをそのまま返すようになった。
従来のActionDispatch::Request#content_typeが返す値にはcharsetパートが含まれていなかった。この振る舞いを変更して、charsetパートをそのままの形で含むContent-Typeを返すようにした。MIMEタイプだけが欲しい場合はActionDispatch::Request#media_typeを使っていただきたい。

# 変更前
request = ActionDispatch::Request.new("CONTENT_TYPE" => "text/csv; header=present; charset=utf-16", "REQUEST_METHOD" => "GET")
request.content_type #=> "text/csv"
# 変更後
request = ActionDispatch::Request.new("Content-Type" => "text/csv; header=present; charset=utf-16", "REQUEST_METHOD" => "GET")
request.content_type #=> "text/csv; header=present; charset=utf-16"
request.media_type   #=> "text/csv"

Rafael Mendonça França
Changelogより大意

🔗 LogSubscriberを設定したコントローラでthrowしたときのエラーを修正


つっつきボイス:「プルリクメッセージを見ると、コントローラで投げた例外がどこで拾われるかという話になっている」「リグレッションが発生したのを修正したのか: &を追加している↓のを見ても、これは改善ではなく修正でしょうね」「修正前はnil参照みたいになる場合があったということかな」

# actionpack/lib/action_controller/log_subscriber.rb#L26
-       if status.nil? && (exception_class_name = payload[:exception].first)
+       if status.nil? && (exception_class_name = payload[:exception]&.first)
          status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
        end

「LogSubscriberをコントローラに設定し、そのコントローラのアクション内でraiseすると、raiseされた例外がLogSubscriberに吸い込まれて、その後コントローラに投げ返してくれなくなったらしい」

f0fdeaa↓を見ると、firstを呼ぶ前のpresent?による存在確認を省略したことでこのリグレッションが起きたようですね」「なるほど、これですか」「一見問題なさそうな最適化が思わぬところに影響したのか」

# actionpack/lib/action_controller/log_subscriber.rb#L25
        if status.nil? && payload[:exception].present?
          exception_class_name = payload[:exception].first
        if status.nil? && (exception_class_name = payload[:exception].first)
          status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)

コントローラのアクションでthrowが使われていると、マッチするものがRackミドルウェア周辺でキャッチされても:exceptionがイベントペイロードにまったく現れなくなっている。
理由は、ActiveSupport::Notifications::Instrumenter.instrument:exceptionをrescueハンドラー内で設定しているため。しかしrescueは以下のようなthrow/catchシナリオでは決して呼び出されない。

catch(:halt) do
  begin
    throw :halt
  rescue Exception => e
    puts "rescue" # ここに到達することはない
  ensure
    puts "ensure"
  end
end

失われたexceptionは実際にはRails 6.1.0より前のバージョンでは扱えていたが、f0fdeaaの最適化のときに、:exceptionが存在することを前提とする部分が更新された。そのコミットを取り消すのが修正としては最も簡単。そういうわけで本PRはリグレッション修正とみなせる。
なお、この問題はRodauthをRailsで使っていて見つけた。
同PRより大意

jeremyevans/rodauth - GitHub

🔗 media=screenstylesheet_link_tagのデフォルトから削除


つっつきボイス:「stylesheet_link_tagを使うとCSSがmedia=screenになってた、ということ?」「それをデフォルトから外してデフォルトがallになったようですね」

# 変更前

stylesheet_link_tag "style"
#=> <link href="/assets/style.css" media="screen" rel="stylesheet" />

# 変更後

stylesheet_link_tag "style"
#=> <link href="/assets/style.css" rel="stylesheet" />

「プルリクタイトルにlegacyって書いてありますけど、media=screenって古いんでしょうか?」「従来はデフォルトでmedia=screenだったけど、わざわざ指定する意味がないから外したのかな?」

参考: メディアクエリの使用 - CSS: カスケーディングスタイルシート | MDN

「DHHが立てたissue↓にこんなことが書かれてる」

これを修正しよう:

歴史的理由によって、media属性は常にデフォルトで”screen”に設定されるので、すべてのメディアタイプを適用するにはスタイルシートで明示的に”all”を設定しなければならない。

stylesheet_link_tag "http://www.example.com/style.css"
# => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" />

config.assets.stylesheets.media_default的なものを追加したうえで、既存のアプリではデフォルトを”screen”にすればよいが、新しいアプリでは意味がないので、その後でmedia: "all"による上書きをscaffoldテンプレートから削除しよう(訳注: 本PRで削除されたそうです)。
#41213より(by DHH)

「今のissueに書かれている情報を見た限りではですが、従来のようにデフォルトがmedia=screenだとそうした部分が直感的でないから、デフォルトをallにすることでブラウザ画面用のCSSをデフォルトで印刷にも使うよう修正したんだろうと思います」「たぶんそうでしょうね」

「普段あまり気にしない部分ですが、数年に一度ぐらいはmedia="print"を指定することもあったので、stylesheet_link_tagと書いただけなのにmedia="screen"が付けられるより、その方が自分も嬉しいですね」「たしかに」

🔗 rescue_fromですべてのエラーを拾えるよう修正


つっつきボイス:「なるほど、rescue_fromで拾う例外が従来StandardErrorだけだったのを、すべての例外を拾えるようにしたようですね↓」

このコミットより前は、rescue_fromハンドラで扱われる例外はStandardErrorだけだった。
この変更によって、rescue句がすべてのExceptionオブジェクトをキャッチするようになり、StandardErrorを継承していないExceptionクラスでrescueハンドラーを定義できるようになる。
つまりStandardErrorの外にあるExceptionをrescueするrescueハンドラーは、今回の変更前に扱えなかった他の例外もrescueできるようになるだろう。
同PRより大意

「たしかRubyの仕様では、改修前のように単にrescue => exceptionと書くとStandardErrorを拾うんですよ」「あ、そうでしたっけ」

# activejob/lib/active_job/execution.rb#L41
    def perform_now
      # Guard against jobs that were persisted before we started counting executions by zeroing out nil counters
      self.executions = (executions || 0) + 1
      deserialize_arguments_if_needed
      run_callbacks :perform do
        perform(*arguments)
      end
-   rescue => exception
+   rescue Exception => exception
      rescue_with_handler(exception) || raise
    end

「Rubyのドキュメントにも書かれてるのを見つけました↓」「ホントだ」「つまりRubyの仕様がそうなってます: 一般にはStandardErrorを継承しますけどね」

参考: 制御構造 (Ruby 3.0.0 リファレンスマニュアル)

error_type が省略された時は StandardError のサブクラスである全ての例外を捕捉します。Rubyの組み込み例外は(SystemExitInterrupt のような脱出を目的としたものを除いて) StandardError のサブクラスです。
docs.ruby-lang.orgより

Exceptionを継承するのって非推奨なんでしょうか?」「非推奨とまではいかなかったと思いますが、Rubyでアプリケーションの例外を扱うときはStandardErrorを継承するのが一般的だったと思います」「なるほど、アプリ開発のお作法的な感じですか」

「RubyのExceptionのクラス階層↓を見るとわかりますが、ExceptionStandardErrorより上位にあるので、改修後のようにrescue Exception => exceptionと書くとすべての例外を拾えるようになります」「なるほど!」

参考: library _builtin (Ruby 3.0.0 リファレンスマニュアル)


docs.ruby-lang.orgより

Exceptionの直下にあるNoMemoryErrorNotImplementedErrorSecurityErrorのような例外クラスはStandardErrorでは拾えません」「なるほど理解できました」

NoMemoryErrorあたりはアプリで表示する可能性がゼロではないかもしれませんが、SyntaxErrorはコードが解析される時点のエラーなので、まずアプリでは表示されないでしょうね」「アプリが立ち上がる前のエラーだからそうなるのか」

「そういう例外を拾えるように改修したということですね」「なるほど」

「何かの機会に、どうも取れない例外があるなって気づいて修正したのかもしれませんね: たとえばNotImplementedErrorなら、DI(Dependency Injection)を使うつもりでスタブのクラスを書いているときに、まだメソッドが実装されてないのにエラーが出なくておかしいなと思って気づく可能性がそこそこありそう」「ありそうですね」

参考: 依存性の注入 - Wikipedia

🔗 Active Storage向けのfixtureサポートを強化

fixture統合を進めるためのActiveStorage::FixtureSetActiveStorage::FixtureSet.blobが宣言されるようになった。
Changelogより


つっつきボイス:「お、Active Storageのfixtureサポートが増えた」「ActiveStorage::FixtureSetが追加されてActive Storage向けのfixtureが使えるようになったみたい」「Active Storageのテストでfixtureが添付ファイルも扱えるようになったということですね」「これはありがたい」「今まで添付ファイルってどうやってテストしてたんでしょうね」「使えるなら欲しいヤツ、いい機能だと思います👍

Rails API: `ActiveRecord::FixtureSet`(翻訳)

🔗Rails

🔗 RailsはRuby 3でどのぐらい速くなったか(Ruby Weeklyより)


つっつきボイス:「RubyやRailsのパフォーマンス記事でお馴染みのNoah Gibbsさんの新しい記事です」

Railsアプリに最適なAWS EC2インスタンスタイプとは(翻訳)

「記事を眺めた感じでは、Ruby 3.0にしたときのパフォーマンスの違いは微差というしかなさそう」「誤差の範囲っぽいですね」

「Ruby自体はRuby 2.0か3.0でら3倍速くなったんですよね」「記事下にあるカラフルなグラフがわかりやすそう↓」「お、なるほど」「グラフを見る限りでは、Ruby 2.7から3.0では実質ほぼ変わらないかな」


同記事より

「3.0に上げて遅くならなければいいと思います👍」「そうですよね」「もし上げて遅くなるとアップグレードしない人が出てくるかもしれませんが、Ruby 3.0でRailsが遅くなったわけではないからそこは大丈夫だと思います」「上げて大丈夫ですよ〜」

🔗 tzinfo-data

「記事ではtzinfo-data↓あたりでいろいろ苦労したみたい」「tzinfo-dataはrails newするとデフォルトで入ってきますね」「そういえば皆さんtzinfo-dataってどうしてます?自分は速攻で削除してますけど」「tzinfo-dataはWindowsでRailsを動かすためのものですよね」「そうなんです、Macで動かすとwarningが出てくる」

tzinfo/tzinfo-data - GitHub

「そのwarningを見るたびに、そもそもWindows環境でRailsを動かすことはほぼないよねという気持ちになります」「Windows環境でRailsを動かす人が少しでもいるうちは残るんでしょうね…」

参考: bundle installする際のtzinfo-dataのwarningがウザい - Qiita


「そういえば、この間ついにRuby 3.0でRails動かしてみましたよ」「お、どうでした?」「まだ動かしてヤッタバンザイしただけ😆」「気持ちわかります」「お気持ちお気持ち」「なおwaringやマイグレーション時にいろいろエラーが出ました」

🔗 ReactとRailsの新しいアーキテクチャ(Ruby Weeklyより)


つっつきボイス:「ReactとRailsの共存を進めている開発会社の記事のようです」「この図で見当が付きそう↓」


同記事より

「図を見た限りでは、現在のアーキテクチャ↑ではReact UI ViewでRailsのRESTful APIにアクセスしている: まさに伝統的なRailsアプローチ」

「そして今後目指すアーキテクチャの図↓では、従来のRailsバックエンドとビューもあるけど、React側にAPIライブラリをはさんだうえで、ReactがアクセスするバックエンドをRailsのRESTful APIから徐々にGraphQLに移行しようとしているようですね」「へ〜」「全部移行するのかもしれませんし、RailsのRestAPIも残すのかもしれませんが、いずれにしろRailsを一度引き剥がしてやり直したい感じがしますね」


同記事より

「このようなアーキテクチャ移行は最近よく見かけるやつですね」

🔗 GraphQLよもやま話

「今日ちょうどWebチームミーティングでRailsとGraphQLについての発表があったんですよ」「お、GraphQLやってるんですね」「自分もGraphQLに関わってみた感想としては、ReactとGraphQLの組み合わせはたしかに便利: でも便利であるがゆえに、GraphQLのエンドポイントで行う仕事が肥大化してくるんですよ」「何となくわかります」

「たとえばさっきの図で言うと、federated GraphQLの下に置くマイクロサービスを1個にするのか、それとも複数のマイクロサービスを扱うのかという選択肢があります」「ふむふむ」「そして後者を選ぶと、ファットなRailsモノリスの代わりにファットなfederated GraphQLができる、そういう未来をちょっと感じているところです」「自分もまさにそう思っています」

「具体的には、このfederated GraphQLで何もかもやろうとするとGraphQLのスキーマが巨大になるんですよ」「やっぱり育っちゃうんですね..」

「Railsの場合は、ルーティングにresourcesと1行書くだけでRESTfulなルーティングが使えますが、GraphQLはQueryとMutationがあって、しかもRestfulのCREATEやUPDATEなどもMutationで定義することになります」「ふむふむ」「それをやっていくと、GraphQLのフラットな名前空間に大量のGraphQLメソッドができてくるんですよ」「ありゃ〜」「ネストすることもできるんですが、いずれにしろ、GraphQLのスキーマはちょっと大きすぎじゃないかなと感じているところです」「うーむ」

参考: クエリのクエリとミューテーションの実行 - AWS AppSync
参考: GraphQLのクエリを基礎から整理してみた - Qiita

「そうやってGraphQLスキーマが育ちすぎたら当然エンドポイントを分割しようという話が出てきますけど、今度は分割されたエンドポイント同士のセッション管理をどうするかという問題も出てくるんですよ」「エンドポイントごとのログイン状態のようなstateをフロントエンド側のコードで判断するのは大変そう…」

「そうなったらAWSのAPI Gateway的な方法を使うことになってくるでしょうね: Railsウォッチでも何度か話題にしたEnvoy(ウォッチ20190212)も、そういう流れで出てきたんだろうと思っています」「なるほど」「きれいにやろうと思ったら、さっきの図で言うReactのAPI Libraryの層の下にもうひとつAPI Gateway的な層も必要になってくるんじゃないかと思ったりしますね」「また層が増えるのか〜」「というようなことを最近GraphQLを使いながら思いました」

参考: Amazon API Gateway(規模に応じた API の作成、維持、保護)| AWS
参考: Envoy Proxy - Home


「質問です: 図のfederated GraphQのfederatedは、ここではどういう状態を指すんでしょうか?」「技術用語ではfederationという言葉がよく使われますが↓、ここで使われているfederated GraphQLは複数のマイクロサービスを取りまとめているGraphQL、という程度の意味じゃないかと思います」「なるほど、辞書的には連合とかそういう意味でしたね」

参考: フェデレーション (federation)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典


前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210126後編)Google Cloud FunctionsがRubyをサポート、Ruby 3のパターンマッチングでポーカーゲームほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20210202後編)Ruby 3 irbのmeasureコマンド、テストを関数型言語のマインドセットで考えるほか

$
0
0

こんにちは、hachi8833です。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsの最新情報などの記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

⚓Ruby

⚓ Ruby 3.0 irbのmeasureコマンド(Ruby Weeklyより)


つっつきボイス:「Ruby 3.0の新機能紹介記事です」「へ〜、irbでmeasureコマンドを実行すると行ごとに時間を測ってくれるのか」「ストップウォッチ的な機能ですね⏱」「たしかに」

# 同記事より
irb(main)> measure
TIME is added
=> nil

irb > sleep 1
processing time: 1.000649s
=> 1

irb > 1
processing time: 0.000025s
=> 1

irb > measure :off
=> nil

参考: library irb (Ruby 3.0.0 リファレンスマニュアル)

「Ruby 3.0の新機能がてんこもりで、この機能に気づかなかった↓」

参考: Ruby 3.0.0 リリース


ruby-lang.orgより

「お、記事によるとmeasureコマンドにstackprof↓を指定するともっと細かく調べられるのか」「irbでこういう機能が使えるようになると、ちょっとした測定が手軽にやれて便利👍」「この辺の機能を使いこなせると強くなった気持ちになれそうですね」

tmm1/stackprof - GitHub

「Ruby 3.0の機能なので新しい環境でしか使えませんけどね」「Ruby 3.0の新機能、がっつり調べないといけないな〜」「パターンマッチングとかもですね」


後で記事のスニペット↓を動かしてstackprofをやってみました。スニペットではさりげなくRuby 3.0のendレスメソッド定義も使っていますね。

# 同記事より
def snippet()= 10_000.times { Date.parse(Date.today.to_s).year != 2020 }

参考: 改めて整理する Ruby 3.0 に実験的に入る予定のエンドレスメソッド定義構文と右代入演算子について - Secret Garden(Instrumental)

以下は素のRuby 3.0.0でも実行できるよう、gem install stackprof activesupportを実行し、irbでrequire 'active_support/core_ext/date'を実行しました。

3.0.0$ ruby -v
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin19]
3.0.0$ gem install stackprof activesupport
Fetching stackprof-0.2.16.gem
Building native extensions. This could take a while...
Successfully installed stackprof-0.2.16
Building YARD (yri) index for stackprof-0.2.16...
Done installing documentation for stackprof after 0 seconds
Fetching activesupport-6.1.1.gem
Successfully installed activesupport-6.1.1
Building YARD (yri) index for activesupport-6.1.1...
Done installing documentation for activesupport after 1 seconds
2 gems installed
3.0.0$ irb
irb(main):001:0> require 'active_support/core_ext/date'
=> true
irb(main):002:0> def snippet()= 10_000.times { Date.parse(Date.today.to_s).year != 2020 }
=> :snippet
irb(main):003:0> measure :stackprof
STACKPROF is added.
=> nil
irb(main):004:0> snippet
==================================
  Mode: cpu(1000)
  Samples: 34 (0.00% miss rate)
  GC: 2 (5.88%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
        17  (50.0%)          17  (50.0%)     Regexp#match
         6  (17.6%)           6  (17.6%)     MatchData#begin
         3   (8.8%)           3   (8.8%)     Date.today
         3   (8.8%)           3   (8.8%)     String#gsub!
         2   (5.9%)           2   (5.9%)     (sweeping)
         1   (2.9%)           1   (2.9%)     Integer#div
        29  (85.3%)           1   (2.9%)     Date.parse
         1   (2.9%)           1   (2.9%)     String#[]=
        32  (94.1%)           0   (0.0%)     IRB::Context#evaluate
        32  (94.1%)           0   (0.0%)     IRB.init_config
        32  (94.1%)           0   (0.0%)     StackProf.run
        32  (94.1%)           0   (0.0%)     IRB::Irb#signal_status
        32  (94.1%)           0   (0.0%)     RubyLex#each_top_level_statement
        32  (94.1%)           0   (0.0%)     Kernel#loop
        32  (94.1%)           0   (0.0%)     Kernel#catch
        32  (94.1%)           0   (0.0%)     IRB::Irb#run
        32  (94.1%)           0   (0.0%)     IRB.start
        32  (94.1%)           0   (0.0%)     <top (required)>
        32  (94.1%)           0   (0.0%)     Kernel#load
        32  (94.1%)           0   (0.0%)     <main>
        32  (94.1%)           0   (0.0%)     <main>
        32  (94.1%)           0   (0.0%)     IRB::WorkSpace#evaluate
        32  (94.1%)           0   (0.0%)     Kernel#eval
         2   (5.9%)           0   (0.0%)     (garbage collection)
        32  (94.1%)           0   (0.0%)     <main>
        32  (94.1%)           0   (0.0%)     Object#snippet
        32  (94.1%)           0   (0.0%)     Integer#times
        32  (94.1%)           0   (0.0%)     IRB::Irb#eval_input
=> 10000
irb(main):005:0>

⚓ HexaPDFを最適化した話(Ruby Weeklyより)


つっつきボイス:「こちらの記事は、PDF生成に使うHexaPDF gemでTrueTypeフォントを使うと測定結果↓が妙に遅いので、ランタイムプロファイリングとメモリのプロファイリングを実行し、原因を突き止めて修正するまでをやってますね」「これは泥臭そうな作業…」

Time Memory File size
hexapdf 1x 557ms 34.160KiB 452.598
hexapdf 5x 1.891ms 45.244KiB 2.258.904
hexapdf 10x 3.754ms 57.364KiB 4.517.825
hexapdf 1x ttf 634ms 33.044KiB 549.522
hexapdf 5x ttf 2.335ms 48.908KiB 2.687.124
hexapdf 10x ttf 4.693ms 63.568KiB 5.360.947

同記事より

「修正後のベンチマークはこれか↓」

Time Memory File size
hexapdf 1x 572ms 34.680KiB 452.598
hexapdf 5x 1.840ms 45.352KiB 2.258.904
hexapdf 10x 3.504ms 57.464KiB 4.517.827
hexapdf 1x ttf 542ms 33.540KiB 546.390
hexapdf 5x ttf 2.099ms 43.600KiB 2.670.953
hexapdf 10x ttf 4.016ms 63.584KiB 5.328.382

同記事より

「Rubyコードが遅い原因を突き止めて解決するまでの泥臭くてつらい作業をまとめた、いい記事だと思います👍」「こういう作業をやれる人がチームにいるかどうかで、チームの限界性能が変わってきますよね」「たしかに」

「この記事ではHexaPDFの文字glyphを処理する周辺の効率化を行っているみたい」「つくづく、プログラマーはいろんなことを知らないといけないんだなと改めて思いますね」「PDFの仕様を以前読んだことありますけど、あれはホントわからない」「ホントに😆

参考: TrueType - Wikipedia
参考: PDFテクノロジーセンター | Adobe PDFテクノロジーセンター — PDF仕様などが置かれています

⚓ 関数型言語のマインドセットでオブジェクトをテストする(Ruby Weeklyより)


つっつきボイス:「thoughtbotさんの記事を久しぶりにご紹介します」「この記事はタイトルからして、テストの視点をどこに置くかという話のようですね」「意外と短いかも」

「テストに関する記事を書こうとすると、『どういう視点でテストすべきか』『どういう実装方法でテストを実装するか』など考慮すべきことが増えてきて、いわゆる『風呂敷を閉じられない』状態になりがちなのですが、この記事は1つの視点に絞って短くまとめているのが良さそうですね👍

「お、この記事でfunctionalって言ってるのはどうやら関数型言語のことみたい」「あの難しそうなヤツですか」「ということは、記事で言っているside effect(副作用)も関数型言語における副作用を指すようです」「あ、そういうことか」「このあたりの話は、関数型言語をわかっている人ならもうわかってるのかもしれませんね」

参考: 関数型言語 - Wikipedia
参考: 副作用 (プログラム) - Wikipedia

「それにしても関数型言語が出てくるとは」「mindsetとあるのは関数型言語のマインドセットなのか」「そういう視点でテストを書くということでしょうね: テストの粒度とかテストを書くときの視点って、一般的に説明しようとすると抽象度が上がりがちで、わかったような気がするけど結局わからなかったということになりがちなんですが、『関数型言語の副作用のようなものだ』という視点を持ち込むことで、ある種のテストの書き方を理解しやすくしようという意図をこの記事に感じました」「なるほど」「もちろんテストの書き方の視点はひとつではなく、これ以外にもいろいろあります」

「ところで、副作用というとちょっとネガティブなイメージありますよね」「あるある」

参考: 副作用 - Wikipedia

🔗 その他Ruby


つっつきボイス:「RubyのWebサーバーPumaのメンテナをやっているNate Berkopecさんのツイートが気になったので拾ってみました」

puma/puma - GitHub

「スレッドがこのツイートから始まっているのでそれ以前の文脈はわかりませんが、『あなたはActive Supportのpresent?blank?を普段から使いすぎているか』というような話かな?」「自分はそう思いました: present?blank?を乱用するってどういうことなのかが気になりました」

Railsでnil? blank? empty? present?を使いこなそう

「Active Supportのpresent?blank?は、どういう使い方をするかにもよるんですが、かの有名な『PHPの==による比較』↓に少し通じるところがあるんですよ」「あ、PHPですか」「そういえばJavaScriptの=====も意味が違ってたかも」「JavaScriptの==も厳密でない比較ですね」

参考: 条件式の「==」と「===」の意味と違いの3つのポイント | PLUGMIZE(プラグマイズ)
参考: JavaScript 忘れがちな === と == の違い - Qiita

「よく考えてみると、present?blank?って、存在するのは何なのか、空白なのは何なのかが実はファジーじゃないですか: 調べる対象は文字列の空文字("")かもしれないし、数値のゼロ(0)かもしれない」「あ〜!」「nilかもしれないですよね」

present?blank?は元のオブジェクトが何であってもチェックできるのでとても便利なんですが、空文字かどうかを調べるならString#empty?がありますし、数値がゼロかどうかを調べるならNumeric#zero?がありますし、nilかどうかを調べるにはnil?で調べるべきでしょうね」「う、心当たりが😅

「上のツイートは、『本来そうやってチェックすべき箇所をblank?とかで雑にチェックしてる人はまさかいないよね?』ぐらいの意味合いなんだろうなと自分は受け止めました」「なるほど、ちょっと腑に落ちてきました」

「ちょうどNateさんのツイートの続きにこんなのもあった↓、まさにtruthy/falselyしか気にしてない人、つまりtrueっぽければそれでいい、falseっぽければそれで済ませるという人には、present?blank?をそうやって雑に使って欲しくないということなんでしょうね」「Nateさんの気持ちがちょっと見えてきたかも」

「とりあえずのノリでpresent?blank?を使ってくれるなよと」「わかってて使うならいいけど、わかってないうちに使ってくれるなよと」「overuseって言ってるのはそういうことなんでしょうね」

「でも自分は作り捨てのコードなら割と使っちゃいますけどね😆」「😆」「でもpresent?blank?もActive Supportのメソッドなので、たまに素のRubyで使おうとしたら『あ、Active Support入れないと使えないんだった』と思い出したりということもありましたね」

⚓クラウド/コンテナ/インフラ/Serverless

⚓ sudoに脆弱性が見つかる


つっつきボイス:「BPSの社内Slackに貼っていただいた情報です」「上の脆弱性情報記事、今日見てみたら情報が更新されてる: ありがたい!」

「sudoの脆弱性はLinux OSの”ほぼ”すべてが対象なのがキツい」「Dockerコンテナのようにコンテナ化されたLinuxは、コンテナにsudoが入ってなければ影響はありません」

参考: sudo - Wikipedia

「それにしても久しぶりに影響の大きい脆弱性ですね」「だいぶ前にbashで起きた脆弱性以来かも↓」「ありましたね、bashの脆弱性」

参考: 更新:bash の脆弱性対策について(CVE-2014-6271 等):IPA 独立行政法人 情報処理推進機構 — 2014年の情報です

「まだ詳しく見ていないんですが、今回のsudo脆弱性はsudoのコンフィグを変えて対処できる感じではなさそうなんですよ: と思ったら上の脆弱性情報記事にちょうど書いてあった↓」「バイナリをリネームですって」「脆弱なsudo実行バイナリがユーザーから実行可能な場所にある限り起きちゃうんですね…」

CentOS6を使用されている場合には、
sudoを使えないようにする(バイナリをリネームする等)
が、現時点での緩和策では一番確実だと思われます。
同記事より

「おそらく今回の脆弱性は、sudoをアップデートせずに回避するのが難しそうです: 特定の機能に潜む脆弱性ならコンフィグでその機能を無効にすることで基本的に回避できますが、上の脆弱性情報記事を見る限りではsudoをユーザー権限で実行できさえすれば攻撃できてしまうように見えますね」「う〜む」「これはエグい」

「それにしては比較的世の中が静かなよう見えますが、今後どこかのタイミングで急に騒がしくなるかもしれませんね」「情報出てから24時間ぐらいしか経ってないからかなと思ったら、数日経ってるんですね」「sudoの脆弱性は今後も見守っておきたいです」

⚓JavaScript

⚓ mongoose: Node.js向けMongoDBクライアント&ライブラリ

Automattic/mongoose - GitHub


つっつきボイス:「こういうのがあると知人から教わりました」「Node.jsでMongoDBを扱う、こういうのは前からありそうかな」「バージョン5とあるぐらいだから前からあるんでしょうね」「よく見ると★も異様に多い」

参考: The most popular database for modern apps | MongoDB


以下はつっつき後に見つけたツイートです。

⚓言語/ツール/OS/CPU

⚓ 言語は継続


つっつきボイス:「各種プログラミング言語のソースをコレクションですか」「さすが言語を愛する人」「私が手伝っているGoby言語↓も恥ずかしながら開店休業に近い状態です😅

Goby: Rubyライクな言語(2)Goby言語の全貌を一発で理解できる解説スライドを公開しました!


後編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210201前編)Webpackerのガイドがマージ、RailsはRuby 3でどのぐらい速くなったかほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

Ruby 3.0でアドベント問題集を解く(1日目)修正のレポート(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Ruby 3.0でアドベント問題集を解く(1日目):修正のレポート(翻訳)

Ruby 3.0が先頃リリースされました。3.0に盛り込まれた楽しい機能をじっくり時間をかけて試すには絶好の時期ですね。

本シリーズでは、Ruby 2.7や3.0のさまざまな機能がAdvent of Codeの問題を解くうえでどんなふうに活用できるかを見ていくことにします。なお、ここに載せる解答例は新機能のデモが目的であり、必ずしも効率が最大とは限りませんのでご了承ください。

記事がだんだん長くなってきたのでカレンダーの日付ごとに分割します。読むのに10分以上かかる記事を増やすのも気が引けますので。

何はともあれ、早速やってみましょう!

⚓ 0日目: ランナースクリプト

「Rubyの記事なのに何でbashが?」とお思いかもしれませんが、まあそう言わずにちょっとご覧ください。

私はコードを手っ取り早く動かせるようにこちらのbashスクリプトを使っています。巧妙に書かれたbashのコードにありがちな、ちょっと怖そうなハックが使われています。

#!/bin/bash

# ここではbashを使っています。

# 数字で始まるファイルを検索する(大したことはしてません)
# 実際のスクリプトファイルにはちゃんと番号を付けてあります
matching=`find . -type f -name "$1_*.rb" | head -n 1`

# ファイル名の拡張子を削除する
name=`basename $matching .rb`

# 続いてRubyに処理を投げる:
# inputs/ディレクトリにある入力のファイル名は変えず、拡張子を.txtにする
ruby ./"$name".rb ./inputs/"$name".txt

入力元は./inputsディレクトリにある.txtファイルたちで、Rubyの出力と同じファイル名になります。これらは捨ててもいい前提ですが、ここでやりたいのはRubyを動かすことなので、このぐらいにして話を元に戻します。

⚓ 1日目: 修正のレポート

最初は、足すと2020になる2つの数字を入力ファイルの中から探す問題です。

完全な解答は以下をご覧ください。

参考: advent_of_code_2020/01_report_repair.rb at main · baweaver/advent_of_code_2020

⚓ 2つの数列を対応付ける

この種の問題で最もよく使われる解法は、値と値を足すと2020になるのに必要な対応付けを(Hashで)作ることです。

TARGET = 2020

n = 1500

duals = {}
duals[TARGET - n] = n
# => { 520 => 1500 }

なぜこのような順序でペアを作るかと言うと、次の数字を与えればいつでも以下のように値を取得できるからです。

n = 520
duals[n]
# => 1500

このハッシュには、足すと2020になることがわかっているペアがひとつ入っているので、その時点で処理を終えられます。このアイデアを完全に実装すると次のような感じになるでしょう。

# 数字を「探索しない」この方法に名前があったら教えてください
# ただし出力関数がエラー時にクラッシュしないよう
# 互換性を取ること
NOT_FOUND = [-1, -1]

def duals(input, target: 2020)
  # 入力をintに変換してハッシュに乗せる
  input.map(&:to_i).each_with_object({}) do |v, duals|
    # 数字のペアが見つかったら、それと`v`を返す
    # それが答えになる
    return [v, duals[v]] if duals[v]

    # 見つからない場合はペアを登録して次回見つけられるようにする
    duals[target - v] = v
  end

  # それ以外の場合は"not found"を返す
  NOT_FOUND
end

⚓ endレスメソッドをラッパーにする

今回の出題では、2つの数値の積も求める必要があります。このduals関数の中で積を求めることもできなくはありませんが、関数に余計な責務を負わせてしまいます。それに、積ではなく単に数値のペアが欲しいときもあるかもしれません。そういうわけで、この関数はこのままにしておくべきです。

積を求める別の関数に上の関数をラップするというのはどうでしょう?Ruby 3.0に導入されたendレスメソッドは、こういうちょっとしたコーディングにうってつけです。

def dual_product(input) = duals(input).reduce(1, :*)

原注:
上でreduce1を渡している理由がおわかりでしょうか?もし仮に空の配列を渡したときにもまともな値を返して欲しいですよね。加算ならば0に何を足しても同じ数値になりますが、乗算でも1で同じことがやれます。
なおこの概念には「identity(単位)」または「empty(零)」という名前があることが前提ですが、詳しくは別記事に譲ります。

endレスメソッドを使ったことで、duals関数を汚すことなく「2つの入力の積を取る」という概念を手軽にラップできるようになりました。とは言うものの、個人的には同じ引数を2回も入力するのは好きではありません。しかしRuby 3.0にはそういうときのための機能もあるのです。

⚓ 引数の転送

Ruby 3.0には、以下のような...という引数を転送する機能(argument forwarding)が導入されました(訳注: 引数の転送は厳密にはRuby 2.7.0で導入され3.0で改良されました)。

def product_duals(...) = duals(...).reduce(1, :*)

上では、product_duals()のすべての引数を次の関数にまるっと転送しています。なお、以下の書き方が無効なのがちょっぴり気がかりです。

def product_duals(input) = duals(...).reduce(1, :*)
# SyntaxError ((irb):22: unexpected ...)

個人的にはこの書き方は可能であるべきだと思います。さもないと、その関数が具体的にどんな引数を取るかを明示的にできず、ひたすらパススルーするだけになってしまいます。これはバグレポートにできそうなので、何か方法を見つけるか作り出せたらここにリンクするつもりです。

今の話題はそのまま次のセクションにつながります。次ではこの入力を取得する必要があります。

⚓ 次は番号指定パラメータを使う

訳注: 番号指定パラメータはRuby 2.7.0で導入されました(NEWS for Ruby 2.7.0)。

私のスクリプトでは、Advent of Codeに掲載されている入力データを含むテキストファイルの取り込みにARGV[0]を使っています。

File.readlines(ARGV[0]).then { puts product_duals(_1) }

上で最初に行っているのはコマンドライン引数の取得(ARGV[0])で、ここで入力ファイルを受け取ります。File.readlinesでファイルのすべての行をArrayとして取得し、それをthenという興味深い関数にパイプしています。

thenはRuby 2.6でyield_selfのエイリアスとして導入されたもので、tapと対照的な操作と考えるとよいでしょう。

訳注: Object#yield_selfはRuby 2.5.0で導入され(NEWS for Ruby 2.5.0)、Ruby 2.6.0でObject#thenというエイリアスが追加されました(News for Ruby 2.6.0)。

1.tap { |v| v + 1 }
# => 1

1.then { |v| v + 1 }
# => 2

tapは元のオブジェクトをそのまま返しますが、thenはそのブロックによる処理結果を返します。ところで、File.readlinesを単純にproduct_dualsでラップすることも一応可能ですが、それでは左から右にスムーズに読めなくなってしまいます。ここでは出力結果には興味がないので、必要に応じてtapthenのどちらでも使えます。

さて、上のコードに_1というものがあることにもお気づきかと思います。これはRubyの「番号指定パラメータ(numbered parameter)」と呼ばれるもので、ブロックの最初の引数を暗に指しています。_1のほかにも_2_3などをもっと増やすことも一応できますが、そこまですることはめったにないでしょう。

ここでは、ファイル入力をパイプで関数に渡すためだけに使っています。

then { puts product_duals(_1) }

そして出力がめでたくSTDOUTからコマンドラインスクリプトに渡されます。これが欲しい答えになります。

⚓ 1日目のまとめ

本日第1日目はここまでにしたいと思います。今後数日間ないし数週間は、Advent of Codeの各問を調べながら解答や手法を探っていきたいと思います。

私のコメント付き解答については以下でご覧いただけます。

baweaver/advent_of_code_2020 - GitHub

関連記事

Rubyでわかる「時計もモノイドの一種」(翻訳)

Ruby 2.5の`yield_self`が想像以上に何だかスゴい件について(翻訳)

Ruby: Enumerableをreduceで徹底理解する#1 基本編(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。


  • 2018/07/17: 初版公開
  • 2021/02/03: 更新

Ruby: Enumerableをreduceで徹底理解する#1 基本編(翻訳)

訳注

原文ではRubyのメソッドを「function」と表記しています。本文中でこれらを(おそらく通常のプリミティブなメソッドと区別する意味で)「高階関数」と呼んでいることから、それに従って本記事では原文に沿って「関数」と表記します。

Enumerableで使える関数のうち、多くのRubyistたちの間で理解がなかなか進んでいないのがreduceです。「これって合計取るぐらいしか使わないよね?」なお、Ruby 2.4以降ならsumで同じことができるようになりました。するとreduceは今や用無しになってしまったのでしょうか?

reduceには秘密があります。「Enumerableの他の関数は、すべてreduceで実装できる」のです。そう、最後にやったことを返すのです。

訳注

reduceには「還元」「削減」「約分」「通分」「値下げ」など多様な意味があり、原文のこの部分では主に「還元」の意味にかかっています。

詳しく見ていく前に、Rubyに備わっている他の高階関数の動作をざっと見てみましょう。

map

まずはmapから。mapEnumerableなコレクションに関数を適用するのに使われます。より一般化して言えば、この関数がどの項目にも個別に適用され、最終的に新しいArrayを1つ得られます。

[1,2,3].map { |i| i * 2 }
# => [2,4,6]

ここでご注意いただきたいのは、mapは元のarray(レシーバー)を改変しないという点です。返されるのは新しいarrayです。

select

selectmapと似ていますが、関数を述語(trueまたはfalseを表すもの)として使う点だけが異なります。trueの場合は新しいリストにその要素を含め、それ以外の場合はリストに含めません。

[1,2,3].select { |i| i > 1 }
# => [2, 3]

reduce

深掘りする前に、今回の主役であるreduceの基本的な使い方を押さえておく必要があります。reduceは要するに何をどんなふうに行うかご存知ですか?まずは基本的な合計値算出をやってみましょう。

[1,2,3].reduce(0) { |accumulator, i| accumulator + i }
# => 6

訳注

accumulatorの基本の意味は「蓄積するもの」で、コンピュータ方面では「累算器」やカタカナの「アキュムレータ」などと訳されます。
参考: アキュムレータ (コンピュータ) - Wikipedia

このコードを初めて読む人には少々込み入って見えるので、いくつかの部分に分解します。

[1,2,3].reduce(0)

[1,2,3]というリストがあり、それを初期値0reduceしようというのです。

{ |accumulator, i| accumulator + i }

そこにブロックを1つ渡しています。このブロックはaccumulatoriという2つのパラメータ(ブロックパラメータ)を取ります。accumulatorに最初に入る値は、初期値0か、リストの最初の要素のどちらかになります。

reduceのループを回すたびに、ループを最後に回したときの戻り値がaccumulatorにセットされます。この[1,2,3]というリストの場合、次のように進行します。

 a | i | reduceの結果
---+---+-----------
 0 | 1 | 0 + 1 → 1
 1 | 2 | 1 + 2 → 3
 3 | 3 | 3 + 3 → 6
 6 | - |     -
---+---+-----------

最終的な戻り値: 6

リストの末尾まで到達すると、その結果はただちにaccumulatorに反映されます。reduceの挙動を理解するには、同じ機能が他の言語でfoldLeft(左に向かって畳み込む)という名前で呼ばれていることを知っておくと役に立つかもしれません。基本的に、私たちはこの[1,2,3]というリストを、+という演算を用いて左に向かって「畳み込んで」います。これは次のように見立てることができます。

((((0) + 1) + 2) + 3)

この丸かっこ()たちを取っ払うこともできますが、accumulatorの新しい値の移り変わりを把握したいので、とりあえずこのままにしておきます。

お楽しみとしてですが、同じ処理をLISP言語で書いた場合と比較してもよいでしょう。

(+ (+ (+ (0) 1) 2) 3)

mapreduceで実装する

ここまでの知識を元に、どうやってmapreduceで実装すればよいでしょうか?

ここでreduceに隠された大きな秘密をひとつお教えしましょう。初期値の種類は何でも構わないのです。

たとえば空のarrayを1つ渡したらどうなるかおわかりでしょうか?何の問題もなくそのままスイスイ進みます。値は値であり、reduceは値を1つ受け取るのです。

def map(list, &fn)
  list.reduce([]) { |a, i| a.push(fn[i]) }
end

map([1,2,3]) { |i| i * 2 }
# => [2, 4, 6]

先ほど、「mapはある関数をリストに適用して新しいリストを得る」とご説明したことを思い出しましょう。このreduceでは関数呼び出しの結果を新しいリストにpushし、最後にa(accumulator)を返します。たまたまこのaccumulatorが新しいリストになったわけです。

先ほどのreduceのときと同様、この動作をステップに分解して詳しく見てみましょう。

    a   | i | fn[i]     | reduceの結果
--------+---+-----------+---------------
 []     | 1 | 1 * 2 → 2 | [].push(2)
 [2]    | 2 | 2 * 2 → 4 | [2].push(4)
 [2,4]  | 3 | 3 * 2 → 6 | [2,4].push(6)
 [2,4,6]| - |     -     |       -
--------+---+-----------+----------------

最終的な戻り値: [2, 4, 6]

selectreduceで実装する

同じく、selectも割と簡単に作れます。

def select(list, &fn)
  list.reduce([]) { |a, i| fn[i] ? a.push(i) : a }
end

select([1,2,3]) { |i| i > 1 }
# => [2, 3]

ここで必要なのは、関数がiについてtrueの場合はリストにpushし、それ以外の場合はpushしないでリストをそのまま返し、次のサイクルに備えるという操作だけです。

これもステップに分解して詳しく見てみましょう。

    a  | i | fn[i]         | reduceの結果
-------+---+---------------+---------------------------
 []    | 1 | 1 > 1 → false | false ? [].push(i)  : []
 []    | 2 | 2 > 1 → true  | true  ? [].push(i)  : []
 [2]   | 3 | 3 > 1 → true  | true  ? [2].push(i) : [2]
 [2,3] | - |       -       |             -
-------+---+---------------+---------------------------

最終的な戻り値: [2, 3]

findreduceで実装する

しかしこの動作は、findで欲しい結果が早々に得られたらそこで処理を終了する、といった場合にはあまり向いてなさそうです。結果が出たのに処理を続行するのはいかにも馬鹿馬鹿しいですよね。そこで休憩がてら😎breakを入れてみましょう。

def find(list, &fn)
  list.reduce(nil) { |_, i| break i if fn[i] }
end

find([1,2,3]) { |i| i == 2 }
# => 2

find([1,2,3]) { |i| i == 4 }
# => nil

ここではreduceの結果をnilにしています。というのも、蓄積された値そのものはどうでもよく、何も見つからなければnilを返したいだけだからです。Rubyのfindメソッドを完全に再現したいのであれば、さらに別の関数を渡さなければなりませんが、それはまたの機会にでも。

さて、breakがあるとどうなるでしょうか?ここでは単にbreakreduceのループから脱出しています。breakは値も返せるのがありがたい点で、必要なら途中でbreakするときに値を渡せます。

  a  | i | fn[i]          | reduceの結果
-----+---+----------------+------------------
 nil | 1 | 1 == 2 → false | break i if false
 nil | 2 | 2 == 2 → true  | break i if true
-----+---+----------------+------------------

breakする場所: 2

  a  | i | fn[i]          | reduceの結果
-----+---+----------------+------------------
 nil | 1 | 1 == 4 → false | break i if false
 nil | 2 | 2 == 4 → false | break i if false
 nil | 3 | 3 == 4 → false | break i if false
 nil | - |        -       |         -
-----+---+----------------+------------------

最終的な戻り値: nil

関数を組み合わせる

きっと皆さんも、これらの関数を組み合わせてみたいと思ったことでしょう。map_compactmap_selectといった具合に、さまざまな関数を自在に組み合わせられるとしたらどうでしょう?

私たちはこのようにレデューサー(reducer)の関数にアクセスできるので、Rubyのあらゆる機能を使ってその決定を下すこともできます。

原注: 何らかの形で関数型プログラミングを嗜んでいて、思わず「(関数の)合成」(composition)と呟いた方へ: 今後の記事をお楽しみに。

それではmap_compactを実装する方法を見てみましょう。

def map_compact(list, &fn)
  list.reduce([]) { |a, i|
    value = fn[i]
    value.nil? ? a : a.push(value)
  }
end

map_compact([1,2,3]) { |i| i > 1 ? i * 2 : nil }\
# => [4, 6]

どことなくselectと近い感じがしませんか?

    a  | i | fn[i]               | reduceで得られるもの
-------+---+---------------------+-------------------------------
 []    | 1 | 1 > 1 : nil         | nil.nil? ? []  : [].push(1)
 []    | 2 | 2 > 1 : 2 * 2 : 4   | 4.nil?   ? []  : [].push(4)
 [4]   | 3 | 3 > 1 : 3 * 2 : 6   | 6.nil?   ? [4] : [4].push(6)
 [4,6] | - |          -          |               -
-------+---+---------------------+-------------------------------

最終的な戻り値: [4, 6]

というわけで、こうやって2つの関数のreduce的な性質をうまく組み合わせられました。ここで何らかの抽象化された振る舞いを得たら面白いと思いませんか?

トランスデューサー(transducer)と呼ばれるものを使えば、さらに際立った楽しさを味わうこともできるようになります。

トランスデューサーは本シリーズの最初の記事の範疇を超えるので、今後の記事をどうぞお楽しみに。

いよいよ私たちは、「something(何かがある)を扱う方法」と「nothing(何もない)を扱う方法」に肉薄しつつあります。関数を組み合わせるときにnothingを構成するものをどのように定義すればよいのでしょうか?今はわからなくとも、おそらくそれはnilfalseではなく、空のリストかゼロを使うことになりそうです。

まとめ

本記事はシリーズ第1回です。次回ではBooleanやNo-Op関数を扱います。

注意

(関数の)合成(composition)などのトピックについては今後の記事で取り上げます。私の元ネタをご存知の方や勘の鋭い方向けに、Dr. River Songの箴言を引用します。

「ネタバレ注意」

関連記事

Ruby: Enumerableを`reduce`で徹底理解する#2 — No-OpとBoolean(翻訳)

Ruby 3のパターンマッチング応用(1)ポーカーゲーム(翻訳)

$
0
0

概要
原著者の許諾を得て翻訳・公開いたします。

本記事内およびサムネイルに用いている素敵なイラストは、原著者の許諾を得て同ブログサイトの同英語記事より引用いたしました。同ブログサイトのイラストはすべてBrandonさん自身が描いたものです。

また、訳文の章立ては原文よりも浅くしました。

参考: トランプ用語一覧 - Wikipedia
参考: List of poker hands - Wikipedia

Ruby 3のパターンマッチング応用(1)ポーカーゲーム(翻訳)

Ruby 3.0の目玉機能としてパターンマッチング(pattern matching)が導入されました。しかしパターンマッチングをどう使いこなせばよいのか、またパターンマッチングがどんなときに欲しくなるか、といった全貌がまだ見えていない方も大勢いらっしゃることでしょう。

本記事では、ポーカーゲームでハンド(hand: 役)のスコアをパターンマッチングで算出する方法をご紹介します。

Arsene Lemur on a playing card

🔗 最終的なプログラム

最初に最終的なスクリプト全体をお目にかけます。その後で、個別のパートについて少しずつ見ていくことにします。

読み進めるうちに少々戸惑う部分もあるかと思いますが、何が行われているのかを頑張って読み取ってみましょう。

Card = Struct.new(:suit, :rank) do
  include Comparable

  def precedence()      = [SUITS_SCORES[self.suit], RANKS_SCORES[self.rank]]
  def rank_precedence() = RANKS_SCORES[self.rank]
  def suit_precedence() = SUITS_SCORES[self.rank]

  def <=>(other) = self.precedence <=> other.precedence

  def to_s() = "#{self.suit}#{self.rank}"
end

Hand = Struct.new(:cards) do
  def sort()         = Hand[self.cards.sort]
  def sort_by_rank() = Hand[self.cards.sort_by(&:rank_precedence)]

  def to_s() = self.cards.map(&:to_s).join(', ')
end

SUITS        = %w(S H D C).freeze
SUITS_SCORES = SUITS.each_with_index.to_h
RANKS        = [*2..10, *%w(J Q K A)].map(&:to_s).freeze
RANKS_SCORES = RANKS.each_with_index.to_h

SCORES = %i(
  royal_flush
  straight_flush
  four_of_a_kind
  full_house
  flush
  straight
  three_of_a_kind
  two_pair
  one_pair
  high_card
).reverse_each.with_index(1).to_h.freeze

CARDS = SUITS.flat_map { |s| RANKS.map { |r| Card[s, r] } }.freeze

def hand_score(unsorted_hand)
  hand = Hand[unsorted_hand].sort_by_rank.cards

  is_straight = -> hand {
    hand
      .map { RANKS_SCORES[_1.rank] }
      .sort
      .each_cons(2)
      .all? { |a, b| b - a == 1 }
  }

  return SCORES[:royal_flush] if hand in [
    Card[s, '10'], Card[^s, 'J'], Card[^s, 'Q'], Card[^s, 'K'], Card[^s, 'A']
  ]

  return SCORES[:straight_flush] if is_straight[hand] && hand in [
    Card[s, *], Card[^s, *], Card[^s, *], Card[^s, *], Card[^s, *]
  ]

  return SCORES[:four_of_a_kind] if hand in [
    *, Card[*, r], Card[*, ^r], Card[*, ^r], Card[*, ^r], *
  ]

  return SCORES[:full_house] if hand in [
    Card[*, r1], Card[*, ^r1], Card[*, ^r1], Card[*, r2], Card[*, ^r2]
  ]

  return SCORES[:full_house] if hand in [
    Card[*, r1], Card[*, ^r1], Card[*, r2], Card[*, ^r2], Card[*, ^r2]
  ]

  return SCORES[:flush] if hand in [
    Card[s, *], Card[^s, *], Card[^s, *], Card[^s, *], Card[^s, *]
  ]

  return SCORES[:straight] if is_straight[hand]

  return SCORES[:three_of_a_kind] if hand in [
    *, Card[*, r], Card[*, ^r], Card[*, ^r], *
  ]

  return SCORES[:two_pair] if hand in [
    *, Card[*, r1], Card[*, ^r1], Card[*, r2], Card[*, ^r2], *
  ]

  return SCORES[:two_pair] if hand in [
    Card[*, r1], Card[*, ^r1], *, Card[*, r2], Card[*, ^r2]
  ]

  return SCORES[:one_pair] if hand in [
    *, Card[*, r], Card[*, ^r], *
  ]

  SCORES[:high_card]
end

# --- テスト ------

EXAMPLES = {
  royal_flush:
    RANKS.last(5).map { Card['S', _1] },

  straight_flush:
    RANKS.first(5).map { Card['S', _1] },

  four_of_a_kind:
    [CARDS[0], *SUITS.map { Card[_1, 'A'] }],

  full_house:
    SUITS.first(3).map { Card[_1, 'A'] } +
    SUITS.first(2).map { Card[_1, 'K'] },

  flush:
    (0..RANKS.size).step(2).first(5).map { Card['S', RANKS[_1]] },

  straight:
    [Card['H', RANKS.first], *RANKS[1..4].map { Card['S', _1] }],

  three_of_a_kind:
    CARDS.first(2) +
    SUITS.first(3).map { Card[_1, 'A'] },

  two_pair:
    CARDS.first(1) +
    SUITS.first(2).flat_map { [Card[_1, 'A'], Card[_1, 'K']] },

  one_pair:
    [CARDS[10], CARDS[15], CARDS[20], *SUITS.first(2).map { Card[_1, 'A'] }],

  high_card:
    [CARDS[10], CARDS[15], CARDS[20], CARDS[5], Card['S', 'A']]
}.freeze

SCORE_MAP = SCORES.invert

EXAMPLES.each do |hand_type, hand|
  score = hand_score(hand)
  correct_text = hand_type == SCORE_MAP[score] ? 'correct' : 'incorrect'

  puts <<~OUT
    Hand:  #{Hand[hand]} (#{hand_type})
    Score: #{score} (#{correct_text})
  OUT

  puts
end

訳注

現時点のRuby 3.0.0ではワンライナーのパターンマッチングについてwarningが表示されますが、実行可能です。

warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!

上のスクリプトにはさまざまなものが盛り込まれています。ここだけ読んで理解に不安があってもご心配なく。本記事でこの後詳しく解説いたします。

それでは深堀りを始めます。よろしいですか?

🔗 変動しない部分

まずはスクリプトの定数部から見ていきましょう。

SUITS         = %w(S H D C).freeze
SUITS_SCORES  = SUITS.each_with_index.to_h
RANKS         = [*2..10, *%w(J Q K A)].map(&:to_s).freeze
RANKS_SCORES  = RANKS.each_with_index.to_h

SCORES = %i(
  royal_flush
  straight_flush
  four_of_a_kind
  full_house
  flush
  straight
  three_of_a_kind
  two_pair
  one_pair
  high_card
).reverse_each.with_index(1).to_h.freeze

CARDS = SUITS.flat_map { |s| RANKS.map { |r| Card[s, r] } }.freeze

🔗 スート

定数部の冒頭はスート(suit: カードの種類)です。ショートハンドとして「スペード」「ハート」「ダイヤ」「クラブ」という4つのスートの頭文字だけを取り出して定義しています。

SUITS        = %w(S H D C).freeze
SUITS_SCORES = SUITS.each_with_index.to_h

スートもスコアリングに使いたいので、each_with_index.to_hを用いてスートの強さの順に手早くインデックス化しておきます。こうしておけば、スートの配列全体をコードのあちこちで生書きせずに済みます。

🔗 ランク

定数部の次はランク(rank: 2〜10、J、Q、K、Aまでのカードの順位)です。2からA(エース)までのランクを、「数値」または「絵札やエースの頭文字」で表現します。

RANKS         = [*2..10, *%w(J Q K A)].map(&:to_s).freeze
RANKS_SCORES  = RANKS.each_with_index.to_h

上のmap(&:to_s)は、ランクの要素の型を一貫させています。それを次のeach_with_index.to_hでランクと強さ(優先順位)を対応付けています。ランクがすべて数値なら話は早いのですが、ランクには絵札も含まれている点が少しだけ面倒なので、このように書きました。

🔗 スコア

お次は、ハンドのランキングを一番強いロイヤルフラッシュから最も弱いハイカードまで定義します。

SCORES = %i(
  royal_flush
  straight_flush
  four_of_a_kind
  full_house
  flush
  straight
  three_of_a_kind
  two_pair
  one_pair
  high_card
).reverse_each.with_index(1).to_h.freeze

コードを読むときは上から強い順に並べて読みたいですよね。上のコードでは読みやすさを優先して、reverse_eachで逆順にし、ロイヤルフラッシュが最高得点になるようにしています。これはあくまで読みやすさのための最適化です(余談ですが、この後パターンマッチングが本領発揮しますのであらかじめ予告しておきます)。

with_index(1)はインデックスを0ではなく1から始まるように変えます。そしてto_hはハンドの種類とスコアを対応付けてくれます。欲しかったのはこれです。

🔗 カード

最後に、カードを一組作る必要があります。

CARDS = SUITS.flat_map { |s| RANKS.map { |r| Card[s, r] } }.freeze

このCard(およびクラスのブラケット記法[])はこの後すぐ使います。今私たちがやっているのは、カードのすべてのRANKSを適用した4つのスートをすべて持つSUITSを作ることです。言い換えれば、ジョーカーを除く52枚のカード一組を作ることです。

🔗 Structを追加する

RubyのStructは、完全なクラスが欲しいわけではない場合にとても重宝します。私の場合は、initializeを入力したくない10行程度のコードでStructを使う傾向があります。

Card = Struct.new(:suit, :rank) do
  include Comparable

  def precedence()      = [SUITS_SCORES[self.suit], RANKS_SCORES[self.rank]]
  def rank_precedence() = RANKS_SCORES[self.rank]
  def suit_precedence() = SUITS_SCORES[self.rank]

  def <=>(other) = self.precedence <=> other.precedence

  def to_s() = "#{self.suit}#{self.rank}"
end

Hand = Struct.new(:cards) do
  def sort()         = Hand[self.cards.sort]
  def sort_by_rank() = Hand[self.cards.sort_by(&:rank_precedence)]

  def to_s() = self.cards.map(&:to_s).join(', ')
end

🔗 Structを作成する

Structはどのように振る舞うのでしょうか?Structは以下のように属性のリストを受け取り、シンプルなデータコンテナとして振る舞います。

Card = Struct.new(:suit, :rank)

雑に申し上げれば、上のコードは以下のクラス定義と同等です。

class Card
  attr_accessor :suit, :rank

  def initialize(suit, rank)
    @suit = suit
    @rank = rank
  end
end

でもStructなら同じことをたった1行でやれます。これで私はコードのデモに集中できるというわけです。

🔗 Structはブロックを受け取れる

Structコンストラクタは、Struct内でのメソッド定義のためにブロックを受け取れます。なお、そこまでやるようになったら、それは「クラスを作る方がいいかもよ」の合図の可能性があります。しかし今はStructのままの方が解説を楽しく続けられますし、必要以上にコードを増やしたくありません。

Card = Struct.new do
  def method_on_card
    'foo!'
  end
end

Card.new.method_on_card
# => 'foo!'

# Just an alias for new:
Card[].method_on_card
# => 'foo!'

🔗 Comparableを導入する

カード同士を比較できるようにしたいのと、カードと数値の対応付けが明確でないため、カードのソートにはRubyの助けを少々借りることにします。

Card = Struct.new(:suit, :rank) do
  include Comparable

  def precedence()      = [SUITS_SCORES[self.suit], RANKS_SCORES[self.rank]]
  def rank_precedence() = RANKS_SCORES[self.rank]
  def suit_precedence() = SUITS_SCORES[self.rank]

  def <=>(other) = self.precedence <=> other.precedence

  def to_s() = "#{self.suit}#{self.rank}"
end

Comparableを用いれば、<=>(宇宙船演算子もしくは比較器)を実装するだけで、Enumerableeachと似た感じであらゆるソートをクラスで使えるようになります。今回使う<=>は以下のようになっています。

def <=>(other) = self.precedence <=> other.precedence

上ではRuby 3の1行メソッド(endレスメソッド定義)を使って、あるカードが他のカードよりも強いかどうかを比較しています。ここでは並び順を強さの順として使っています。

def precedence() = [SUITS_SCORES[self.suit], RANKS_SCORES[self.rank]]

ここでArrayが登場する理由は何でしょうか?デフォルトではまずスートでソートし、それからランクでソートしたいからです。スペードはハートより強く、エースは他のランクよりも強いといった具合です。

パターンマッチングでは順序が一貫していることを前提とするので、以下では主にrank_precedenceが頼りとなります。このスクリプトでは通常の並び順は通用しないので、単なるデモ用です。

🔗 to_s

文字列で表現しておくとデバッグでとても有用です。ここではCardをスートとランクで表現しています。以下は何のひねりもない普通のコードです。

def to_s() = "#{self.suit}#{self.rank}"

🔗 ハンド

今度は、今やっていることを正確に把握するために「ハンド」の概念が欲しくなってきました。

Hand = Struct.new(:cards) do
  def sort()         = Hand[self.cards.sort]
  def sort_by_rank() = Hand[self.cards.sort_by(&:rank_precedence)]

  def to_s() = self.cards.map(&:to_s).join(', ')
end

上のコードでは、sortのようなメソッドでオブジェクトを改変するのではなく、新しいオブジェクトを返そうとしていることにお気づきの方もいらっしゃるかもしれません。このスタイルはほぼ関数型言語に近く、テストデータを何度も改変せずに済みます。

クラスやstructは、複雑なものでなければならないという決まりはありません。これらは単にいくつかの値を提供して配列のソートを繰り返し、文字列をいつでも出力できるというだけです。

🔗 ハンドのスコアを算出する

いよいよこのプログラムの本当に美味しい部分にたどり着きました。ここではかなりいろんなことをやっています。

🔗 ハンドをソートする

ハンドのスコアを出すには、パターンマッチングが効くように順序を揃える必要があります。

hand = Hand[unsorted_hand].sort_by_rank.cards

このHandにはおそらくArrayで使われそうなメソッドを追加するので(といってもここではソートしたいだけですが)、Enumerableにしておいてそこにパターンマッチングのフックを追加します。

🔗 ストレートの判定とソート

パターンマッチングでは、ありうるパターンをすべて網羅しておかないと、ストレートを含む手をうまくチェックできません。自分がせっかくハンマーを持っていても、周りにあるのが全部釘だとは限りません。今回はまさにそうしたケースです。そこで、ここではlambda関数をひとつこしらえて、ストレート的なハンドで以下のように数回ほど再利用できるようにします。

is_straight = -> hand {
  hand
    .map { RANKS_SCORES[_1.rank] }
    .sort
    .each_cons(2)
    .all? { |a, b| b - a == 1 }
}

ここでは、カードごとのスコアがどのランクに相当するかを表すために、自分のハンドをmapしたいと思います。ここではスートのことは考えません。続いてハンドをソートし、each_cons(2)で2枚のカードのそれぞれの後続グループを取得したいと思います。

ここでのポイントは、どのペアもカード同士のランクが1つしか離れていないようにしたい、つまりストレートの一部であることを確認したいということです。

🔗 ロイヤルフラッシュ

いよいよ本記事で最も興味津々の部分に差し掛かってきました。まずは以下のようにパターンマッチングを1行に収めてみました。

return SCORES[:royal_flush] if hand in [
  Card[s, '10'], Card[^s, 'J'], Card[^s, 'Q'], Card[^s, 'K'], Card[^s, 'A']
]

ロイヤルフラッシュ(royal flush)は、同一スートのカードが10からAまで連続することを指します。このパターンマッチングでは、sで最初のスートをキャプチャし、その後ろに^s(ピン演算子^s)を記述することで、すべてのカードのスートが同一であるという期待を示します。

たとえば最初のカードのスートがスペードなら、残りのカードのスートもスペードであることを期待します。そうでない場合は次のパターンに進みます。

ところで、Card[...]という記述にお気づきでしょうか。この構文はパターンマッチで属性にアクセスするためのもので、Structに限らずあらゆるクラスで利用できます。この動作の細かなニュアンスについてもっと詳しく説明するには私がもっと実験を重ねる必要がありますが、とりあえず今はStructできれいに動いています。

私は「左から右に進む」スタイルが好みなので、return score if matchという種類の構文にしています。複数行によるパターンマッチを使う手も一応考えられますが、ストレートのチェックがうまくいかなくなって厄介なことになりそうです。

🔗 ストレートフラッシュ

お次もなかなか興味深い内容です。もしここでパターンマッチングを使いすぎると、コードが短くならずに逆に増えて見づらくなる可能性もあります。そこで、ここでは先ほどのストレートチェックを元にしています。

return SCORES[:straight_flush] if is_straight[hand] && hand in [
  Card[s, *], Card[^s, *], Card[^s, *], Card[^s, *], Card[^s, *]
]

ハンドがストレートで、しかもカードのスートがすべて揃っていれば勝ちです。s^sを使っている点は先ほどと同様です。初登場の*は、値はここでは重要ではないのでそのままにすることを表します。

🔗 フォーカード

フォーカード(原文ではFour of a Kind)は、カードのランクは同じでスートがどれも異なるというハンドです。

return SCORES[:four_of_a_kind] if hand in [
  *, Card[*, r], Card[*, ^r], Card[*, ^r], Card[*, ^r], *
]

rは上述のsと同じアイデアです。そして残りの部分はピン演算子でランクをピン留めすることで、4枚のカードのランクが同じであることを記述します。ここではスートは重要ではありません。一組のカードの中にある同じランクのカードは4枚ずつありますが、4枚のどれもスートが異なることはわかりきっているので、同じランクのカードを4枚キャプチャすれば自動的にスートは異なるものになります。

今度は*の置き場所が少し変わっていることにお気づきでしょうか。ここでは*を2箇所で探索に使っています。フォーカードは自分のハンドのうち、4枚のカードで成立すればよいので、ハンドの冒頭や末尾は任意のカードでよいことを*で表しています。つまり、たとえばAAAAKKAAAAというパターンはいずれもフォーカードとして有効です。

🔗 フルハウス

これも興味深い内容です。しかもコードが少々これまでと趣が異なっています。

return SCORES[:full_house] if hand in [
  Card[*, r1], Card[*, ^r1], Card[*, ^r1], Card[*, r2], Card[*, ^r2]
]

return SCORES[:full_house] if hand in [
  Card[*, r1], Card[*, ^r1], Card[*, r2], Card[*, ^r2], Card[*, ^r2]
]

パターンが2つある理由がおわかりでしょうか?最初の理由は「|(論理ORを表します)を使う場合は、名前付きキャプチャやピン演算子が使えない」からです。ここでは無理せずマッチを2つに分けなければなりません。

フルハウスは、言ってみれば「スリーカード(Three of a Kind)」と「ワンペア(原文ではTwo of a Kind)」の合わせ技で、AAABBAABBBのように2つの異なるパターンでできています。1つ目のコードでは、冒頭の3枚のカードをr1で、末尾の2枚のカードをr2で表しています。そして2つ目のコードではそれらを入れ替えてチェックしています。

🔗 フラッシュ

フラッシュでは、自分のハンドのスートがすべて揃っていることを確認します。

return SCORES[:flush] if hand in [
  Card[s, *], Card[^s, *], Card[^s, *], Card[^s, *], Card[^s, *]
]

sを使っている点はこれまでと同様です。残りのカードのスートがすべて冒頭のカードのスートと同じであることを^sで表しています。

🔗 ストレート

ストレートでは、カードのランクが飛び番にならずにひと続きになっていることを確認します。カードのスートは揃っていなくてもよい点がストレートフラッシュと違います。このときのために、先ほどlambda関数を仕込んでおいたのです。

return SCORES[:straight] if is_straight[hand]

🔗 スリーカード

スリーカード(原文ではThree of a Kind)は上述のフォーカードとよく似ていますが、同じランクのカードが4枚ではなく3枚あればよい点が異なります。

return SCORES[:three_of_a_kind] if hand in [
  *, Card[*, r], Card[*, ^r], Card[*, ^r], *
]

3枚のカードのうち、冒頭のカードのランクをrでキャプチャして残りの2枚のカードをピン留めします。冒頭と末尾の*は、真ん中で定義されている3枚のカードが隣り合っている限り、残りの2枚のカードは何でもよいことを表します。たとえばKAAAQKQAAAAAAKQは、いずれもエースが3枚連続で並んでいるのですべて有効です。

🔗 ツーペア

ツーペアはフルハウスと似ていますが、スリーカードがない点が異なります。

return SCORES[:two_pair] if hand in [
  *, Card[*, r1], Card[*, ^r1], Card[*, r2], Card[*, ^r2], *
]

return SCORES[:two_pair] if hand in [
  Card[*, r1], Card[*, ^r1], *, Card[*, r2], Card[*, ^r2]
]

ここでは、異なるペアが2つあることを確認するために、r1^r1r2^r2をそれぞれ用いています。その次のコードは、言ってみればセカンドマッチというトリックです。異なるペアが2つある場合、2つのペアの間に別なカードが挟まる可能性があるので、それをキャプチャするのに使います。

カードはソート済みという前提で進めているので、ここまで何の問題も生じていません。でもここでひとつお楽しみの機能をご紹介しますので、必要になりましたらお使いください。

🔗 ワンペア

チェックがあとひとつ残っていましたね。

return SCORES[:one_pair] if hand in [
  *, Card[*, r], Card[*, ^r], *
]

上のコードでやっていることはこれまでと同様ですが、同じランクのカードが2枚、つまりペアがひとつありさえすればワンペアになります。スートをチェックする必要がない点もこれまでどおりです。

🔗 ハイカード

いよいよ最後のハンドです。他のどれにもマッチしない場合はハイカード(High Card: 日本ではノーペアとも呼ばれるようです)を返し、カードをソートすることでハイカード同士のどちらが最終的に点が高いかを示します。

SCORES[:high_card]

これでスコアが取れました!

🔗 テストと実行例

後はテストコードをいくらか追加して、すべてうまく動いていることを確認するだけです。

EXAMPLES = {
  royal_flush:
    RANKS.last(5).map { Card['S', _1] },

  straight_flush:
    RANKS.first(5).map { Card['S', _1] },

  four_of_a_kind:
    [CARDS[0], *SUITS.map { Card[_1, 'A'] }],

  full_house:
    SUITS.first(3).map { Card[_1, 'A'] } +
    SUITS.first(2).map { Card[_1, 'K'] },

  flush:
    (0..RANKS.size).step(2).first(5).map { Card['S', RANKS[_1]] },

  straight:
    [Card['H', RANKS.first], *RANKS[1..4].map { Card['S', _1] }],

  three_of_a_kind:
    CARDS.first(2) +
    SUITS.first(3).map { Card[_1, 'A'] },

  two_pair:
    CARDS.first(1) +
    SUITS.first(2).flat_map { [Card[_1, 'A'], Card[_1, 'K']] },

  one_pair:
    [CARDS[10], CARDS[15], CARDS[20], *SUITS.first(2).map { Card[_1, 'A'] }],

  high_card:
    [CARDS[10], CARDS[15], CARDS[20], CARDS[5], Card['S', 'A']]
}.freeze

SCORE_MAP = SCORES.invert

EXAMPLES.each do |hand_type, hand|
  score = hand_score(hand)
  correct_text = hand_type == SCORE_MAP[score] ? 'correct' : 'incorrect'

  puts <<~OUT
    Hand:  #{Hand[hand]} (#{hand_type})
    Score: #{score} (#{correct_text})
  OUT

  puts
end

条件にマッチするハンドをいくつかテストコードに盛り込んであります。いくつかについては、ハンドの残りの部分を有効な範囲で任意のカードで埋めています。カードを埋めるときは、誤って他のハンドができないように注意しつつ自分の好きな数字を入れています。

これまで触れていなかった部分について駆け足で説明します。

🔗 番号指定パラメータ

Ruby 3では以下のような「番号指定パラメータ(numbered parameter)」という機能が導入されました。

RANKS.last(5).map { Card['S', _1] }

上は「最もランクの大きい5枚のカード」「スートはすべてスペード」を表します。この_1は、関数の第1パラメータを暗に示しています。

🔗 splat演算子

Rubyのsplat演算子*は、配列の要素をばらして関数や別のコレクションに渡すのに使えます。splat演算子で得られるのは、ネストしていない1個のフラットな配列です。私はsplat演算子を使うこともあれば配列の結合を使うこともありますが、本記事のコードでは以下の例のように一貫しているわけではありません。

[CARDS[0], *SUITS.map { Card[_1, 'A'] }]

🔗 first

Rubyのfirstは配列の最初の要素を返します。first(n)とすると、配列の冒頭からn番目の要素を返します。1を指定したときに配列の最初の要素を返してはいけないという決まりはありません。本記事のコードではsplat演算子の代わりに使える前提としていますが、既に申し上げたように本記事のコードでの用法は一貫していません。

CARDS.first(1) +
SUITS.first(2).flat_map { [Card[_1, 'A'], Card[_1, 'K']] }

🔗 step

本記事のコードでは、ストレートがフラッシュと誤認されないよう残りのカードをスキップするために、Rubyのstepを用いています。

(0..RANKS.size).step(2).first(5).map { Card['S', RANKS[_1]] }

ここではインデックスを用いています。

🔗 まとめ

本記事を書くためにだいぶさまよいました。特にコードを適切に実行する部分で苦労しました。皆さんが本記事を楽んでくださり、パターンマッチングについて少しでも得るところがあれば幸いです。

Ruby 3にはまだまだお楽しみがいっぱい隠れています。たまにはRubyで心置きなく遊びましょう!


関連記事

Ruby 3.0でアドベント問題集を解く(1日目)修正のレポート(翻訳)

Rubyでわかる「時計もモノイドの一種」(翻訳)

週刊Railsウォッチ(20210208前編)Rails次期リリースがバージョン7に決定、thoughtbotのアプリケーションセキュリティガイドほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsの最新情報などの記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下のコミットリストのChangelog変更から見繕いました。

🔗 Action Cableクライアントの”thundering herd”を防止


つっつきボイス:「thundering herdって何だろう?」

herd n. 家畜[動物]の群れ; 《軽蔑》 (同じような行動などをする)群集.

「ググったら出てきた↓」「へ〜、こういう状態をthundering herdって言うのか!」「マルチスレッドなプログラムで何も考えずにシグナルを使ったときの現象に似てますね: シグナルマスクせずにシグナルを送ると配下の全スレッドがシグナルに応答してしまうので、どれか一つが応答すればよいだけの場合には無駄が大きいんですが、そのようなメッセージバスを共有するモデルに共通する問題に近いかもしれませんね」

参考: WebサーバでのThundering Herdは過去の話? | monolithic kernel

Thundering Herdというのは、ひとつのソケットに対してselectやepollのような通信可能になるのを待つシステムコールを利用して複数のプロセス (またはスレッド) が待機していると、通信可能になったときに本来ならひとつのプロセスだけが起きればいいところを、待機していたすべてのプロセスが起こされてしまうという問題です。この場合、実際に通信できるのはたまたま最も早く通信を開始したプロセスのみで、他のプロセスが起きたことは無駄になってしまいます。
blog.mono0x.netより

参考: Thundering herd problem - Wikipedia

「このプルリクはAction Cableクライアントが対象なので、そこで発生する可能性のあるthuntering herdとやらを修正するためのものなんでしょうね」「あ〜なるほど」「Action Cableで膨大な数のセッションを管理しているときにこれが起きるとかなりのパフォーマンス低下につながる可能性があるので、こういう問題が解決されるのはよいと思います👍

「プルリクのコードを覗いてみるとJavaScriptのコードの追加が多いですね」「Changelogを見ると再接続と書かれているので↓、やはりクライアント側の修正なんでしょうね」

Action Cableクライアントに、サーバー接続が喪失した後のクライアント再接続における”thindering herd”を防止する以下のセーフガードが含まれるようになった。

  • クライアントは、サーバーへの直近のpingの後から最初の再接続試行までの間、staleスレッショルドのランダムな期間でウエイトをかける。
  • 以後の再接続試行では、(対数的バックオフではなく)指数的バックオフを用いるようになった。再接続試行までの遅延時間の増加を当初ゆるやかにするために、デフォルトの指数の基数部は2より小さい値になっている。
  • 再接続の試行と試行の間の遅延時間には、それぞれランダムなジッターが適用される
    Changelogより大意

参考: 仮数部、指数部、基数:意味と計算方法 - 具体例で学ぶ数学

「なるほど、以前も話したランダムなジッター(jitter)を遅延時間に加えているのか(ウォッチ20191216): これはJavaScriptの変更ですが、サーバー側にも影響するものですね」「おぉ?」「たくさんのクライアントが再接続を同時にかけてくるときは、ランダムなジッターを加えてアクセスが同時に集中しないようにするという手法です」「あ〜、思い出してきました」

参考: ジッター - Wikipedia

「ジッターを加えることでクライアントの再接続が成功する可能性も高まりますし、再接続するまでの時間も短縮できますね」「なるほど!」「ジッターがないと、多数のクライアントが同じ時刻に一斉に再接続をかけてきてしまいます」「これは勉強になりました」

「この問題自体はWebSocketの話のようですが、クライアントサーバー形式の通信では普遍的に起きうる現象ですね」「なるほど」「なお、このプルリクにはbackoff timerのアルゴリズム修正も入っているようですね: プルリクのコメントでかなり細かく具体的なケースのやり取りがされているので、興味ある人はコメントも読んでみると面白いと思います」


特に必要はないと思いますが、thundering herdという現象をうまく表せる既存の熟語が見当たらないので、英語ママとしました。

どうやら元ネタはZane Gray作の西部劇小説「Thundering herd」のようで、以下の映像にはBuffalo Stampedeというサブタイトルが付いていました。要するに時代劇ですね。

参考: ゼイン・グレイ - Wikipedia

🔗 Rails 6.2がRails 7になった


つっつきボイス:「@rafaelfrancaさんのコミットです」「え、Rails 6.1の次はもうRails 7になるんですか?」「どんな目的なんでしょうね?」「昨年末にリリースしたHotwireを入れるのかな?🤔

# Gemfile.lock#L30
PATH
  remote: .
  specs:
-   actioncable (6.2.0.alpha)
-     actionpack (= 6.2.0.alpha)
-     activesupport (= 6.2.0.alpha)
+   actioncable (7.0.0.alpha)
+     actionpack (= 7.0.0.alpha)
+     activesupport (= 7.0.0.alpha)
...

速報: Basecampがリリースした「Hotwire」の概要

「6.0から6.1のようにマイナーバージョンが進むときって、基本的には大きく変わらないものだという通念がありますけど、Railsの場合マイナーバージョン番号が更新されるときにも大きな変更が時たま入ってきたりしますよね」「そう思います😆」「考えてみれば、Rails 6.1は6.0と比べてだいぶ変わりましたよね」「自分でも6.1に上げたときに変更点が割と多い印象でしたね、個人的には大きな支障はありませんでしたが」

「変更が大きかったといえば、やはりRails 3.0からRails 3.1のときでしょう」「そうそう!あれはキツかった」「アセットパイプラインがない世界から3.1でいきなりアセットパイプラインのある世界への移動は大きかった」「breaking changeとしてはでかい」

参考: アセットパイプライン - Railsガイド

「もしかするとrequired Ruby versionを変えるから7にするのかな?」「コミットのリリースノートを見ると↓、Rails 7ではRuby 2.7以上が必須、3.0以上が好ましいとなってる」「ホントだ」

  • Ruby 2.7.0+ required, Ruby 3.0+ prefered
    コミットのguides/source/7_0_release_notes.mdより

「そういえば、ちょうど今年1月の銀座Railsで、Ruby 3.0とRails 6.1のバージョニングについて@yahondaさんが話してたのを思い出しました↓」

参考: 【オンライン開催】銀座Rails#29@リンクアンドモチベーション - connpass

「スライドにも、required Ruby versionが上がるのは少なくともRails 5以降はRailsのメジャーバージョンアップのみ、とありますね」「へ〜!」「これを元に推測するなら、メジャーバージョンアップを決めた背景にrequired Ruby versionを変更したかったというのがあるのかもしれませんね」

「ところで、このコミットのコミットメッセージに”big plans”と書かれているんですが↓、具体的な情報は今のところまったく見当たりませんでした」「big planですか」「big planが何なのかは手がかりなしです」

We have big plans for the next version of Rails and that require big versions.
同コミットより

🔗 disallow_raw_sql!で末尾ホワイトスペースを削除するようになった


つっつきボイス:「なるほど、pluckの末尾に\nなどが入っているとエラーになるので、削除して動くようにしたんですね↓」「末尾のホワイトスペースならメソッド側で除去しても大丈夫そう」

# 変更前
User.pluck("first_name, last_name") #=> SELECT first_name, last_name FROM "users"
User.pluck("first_name, last_name\n") #=> ActiveRecord::UnknownAttributeReference
# 変更後
User.pluck("first_name, last_name") #=> SELECT first_name, last_name FROM "users"
User.pluck("first_name, last_name\n") #=> SELECT first_name, last_name\n FROM "users"

参考: ホワイトスペースとは - IT用語辞典 e-Words

「Railsのpluckに改行入り文字列を渡したことってなかったかも」「自分はpluckで複数カラムを指定するときにカンマ区切り文字列ではなく複数のシンボル引数を渡すようにしているので、このケースでハマることはありませんでしたね」

Rails: pluckでメモリを大幅に節約する(翻訳)

🔗 新機能: primary_abstract_class


つっつきボイス:「タイトルにexposeと書かれているのでprivateなメソッドをpublicにしたのかと思ったら、既存のself.abstract_class = trueprimary_abstract_classで明示的にpublicメソッドで書けるようにしたようですね」

参考: [Rails] self.abstract_class = true の意味と挙動 « Codaholic
参考: Active Record で複数のデータベース利用 - Railsガイド

# Railsガイドより
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to database: { writing: :primary, reading: :primary_replica }
end

self.abstract_class = trueって使ったことなかったかも…」「この書き方は、Railsのマルチプルデータベースなどで使うことがあります: データベースごとにベースクラスを作るけど、そのクラスは直接newさせたくないとき、つまりActive Recordのクラスとして直接使って欲しくないとき」「あ、今理解しました😅

「コードの中でも最終的にself.abstract_class = trueを呼んでいる↓」

      def primary_abstract_class
        if Base.application_record_class && Base.application_record_class != self
          raise ArgumentError, "The `primary_abstract_class` is already set to #{Base.application_record_class}. There can only be one `primary_abstract_class` in an application."
        end

        self.abstract_class = true
        Base.application_record_class = self
      end

「考えてみれば、従来のself.abstract_class = trueのように従来のクラス変数を直接上書きする方式は、Railsガイドに書かれているとはいえやや直接的すぎるという見方もできるので、こうやってメソッドで設定する方がよさそうな気はします」「従来どおりにself.abstract_class = trueを書くこともできそうに見えるけど、メソッドができたなら今後はこれで設定する方がいいのかもしれませんね」


アプリケーションでprimary_abstract_classを設定する方法を提供する。
マルチプルデータベースのRailsアプリケーションで、名前がApplicationRecordでないプライマリ抽象クラスを使っている場合に、特定のクラスをprimary_abstract_classに設定できるようになった。

class PrimaryApplicationRecord
  self.primary_abstract_class
end

アプリケーションが起動すると、自動的にプライマリまたはデータベースコンフィグファイルの最初のデータベースに接続する。マルチプルデータベースでその後connects_toを実行するには、デフォルトのコネクションが ApplicationRecordのコネクションと同じであることを認識する必要がある。しかしアプリケーションによっては ApplicationRecordの名前が異っていることもある。この変更によって、Active Recordが同じデータベースに対してコネクションを複数オープンしてしまうことが防止される。
同Changelogより大意

🔗 Active Storageで定義済みのvariantを使えるようになった

# 同PRより
class User < ActiveRecord::Base
  has_one_attached :avatar, variants: {
    thumb: { resize: "100x100" },
    medium: { resize: "300x300", monochrome: true }
  }
end

class Gallery < ActiveRecord::Base
  has_many_attached :photos, variants: {
    thumb: { resize: "100x100" },
    medium: { resize: "300x300", monochrome: true }
  }
end

<%= image_tag user.avatar.variant(:thumb) %>

つっつきボイス:「Active Storageでようやくvariantを定義して、上のvariant(:thumb)のように名前で呼べるようになったんですね」「variantとは?」「上のコードのthumbmediumのような、Active Storageで扱う画像のサイズの種類のことですね: 100x100などがvariantの設定」「あ、それのことでしたか」

「なお、これはpaperclipやshrineといったgemには普通にある機能です」「そうそう」「Active Storage自体にはvariantの機能が入っていますが(ウォッチ20200114)、この改修が追加されたということは定義済みのvariantを使う機能がこれまでなかったということなんでしょうね」「こうやって他のgemの機能も取り入れてActive Storageがよくなっていくということですね」

thoughtbot/paperclip - GitHub

shrinerb/shrine - GitHub

🔗Rails

🔗 銀座Railsスライド『Active Recordから考える次世代のRuby on Railsの方向性』


つっつきボイス:「上は以前Railsdmで評判を呼んだ『Ruby on Railsの正体と向き合い方』↓の@_yasaichiさんが銀座Rails#29で発表したスライドです」「あれは2年ぐらい前でしたっけ?」「そういえばウォッチでも取り上げたときもとても評判がよかったですね(ウォッチ20190401)」

「前回のスライドは、Ruby on Railsの歴史的経緯を詳しく説明してから、それを踏まえてRailsが今後どう進んでいくのかを考察する内容になっていましたが、今回はフロントエンドも含めて同様の考察を行っています」「おぉ〜!」「Railsのフロントエンド周りの将来像は、Railsをやってる人たちなら誰でも気になるところですね」「これは後で読まなきゃ」「Railsのまとめ方なども含めていろいろ参考になります: RailsをやっていればRailsについて思うことはいろいろあるわけですが、今回のスライドもRailsに入れ込みすぎず突き放しすぎない視点でRailsの現状と将来像を捉えようとしているのがとても参考になります🙏

「なおこの間の銀座Rails29はその後の懇親会がディープな話で盛り上がりましたね」

🔗 1個のファイルでRailsアプリを書く(Ruby Weeklyより)


つっつきボイス:「1個のファイルでRailsアプリを書く…とは?」「1個のファイルでRails書けるかどうかチャレンジするってことかしら?」「routes.rbにlambdaを書いておしまいにするってこと?それだともうRailsではないですよね」「たしかに」

「ああ、Railsのconfig.ruファイルに全部書くということか↓」「あ、理解しました!」「これスクショなのか…」


同記事より

「通常ならapplication.rbやらroutes.rbやらのファイルに分かれているのを、ここではRails::Applicationを継承したクラスを作ってそこに全部書くということらしい」「へ〜!」「スゴいことしますね」「routes.appendは本来routes.rbで行われますけど、そういうのもここに書かれている」

「やろうと思えばRailsでこんなこともできるという記事」「さすがに本番でやろうとは思いませんでした😆」「本当にRailsがシングルファイルアプリになるなら別の意義がありそうですが、この記事のは実用目的ではなさそうかな」

参考: Rails の初期化プロセス - Railsガイド — config.ruについても説明されています

🔗 thoughtbotの「アプリケーションセキュリティガイド」


つっつきボイス:「thoughtbotの記事を追っててこのセキュリティガイドを見つけました」「日付は昨年7月なのか」「それほど長くなさそう」

「へ〜、Personally-Identifying Information (PII)という言葉があるのか: どこまで一般的なものかどうかはわかりませんが、このような個人を特定可能な情報の扱いに注意することは大事ですね」「たしかに」

参考: 個人を特定できる情報 (PII = Personally Identifiable Information) とは何か? - Twilio

「この記事で説明されていることは、セキュリティを考えるうえでどれも当たり前の話ではありますが、こうやってリスト形式で簡潔にまとめられていることで、改めて読んでみると自分の理解が不足している部分を見つけられたりするのがいいと思います👍」「そうですね」「何となくわかっているつもりのことでも、自分がこの記事のように説明できない部分を見つけたら、改めて調べて勉強しないといけない気持ちになれますね」「自分も頑張らなきゃ」

🔗 Google Cloud Functions


つっつきボイス:「この間のウォッチでも報じたGoogle Cloud Functionsのことですね(ウォッチ20210126)」「そうそう、ツイートにもあるようにRubyで使うbundlerのbundle installをGCP側でやってくれるんですよ」「それ優秀じゃないですか!」

参考: Bundler: The best way to manage a Ruby application’s gems

「簡単なRubyコードを動かす分にはGoogle Cloud Functionsの方がAWS Lambdaよりも便利だと思います👍」「お〜」「AWS Lambdaだと、Lambdaレイヤの状態を考慮しないといけないなど、デプロイに多少ノウハウが必要なんですよ」「なるほど」「コンテナでデプロイするなら別ですけど、場合によってはコンテナだとオーバーキル気味になることもあります」

「RubyをサポートするようになったGoogle Cloud Functionsのいいところは、Rubyのソースコードをそのままアップロードして公開できる点だなと思います」

参考: Cloud Functions  |  Google Cloud

参考: AWS Lambda(イベント発生時にコードを実行)| AWS

🔗 その他Rails

つっつきボイス:「4月にオンライン開催予定のRailsConf 2021の登壇者募集のお知らせです」「CFP(Call for proposal: 登壇者募集)の締め切りが今月15日、ってもうすぐなのか」「発表したい人はどうぞ」

「お、募集ページにCFPのstatsが載ってますね↓」「これは?」「何日に何人応募があったかというグラフ」「こういうグラフはたいてい締切日になるとガガっと上昇します😆」「そういうものですよね、昔から」


同サイトより

参考までに、昨年のRailsConf2020のキーノートスピーチ動画を貼っておきます。


前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210202後編)Ruby 3 irbのmeasureコマンド、テストを関数型言語のマインドセットで考えるほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly


週刊Railsウォッチ(20210209後編)Rubyでミニ言語処理系を作る、Kernel#getsの意外な機能、CSSのcontent-visibilityほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsの最新情報などの記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 Rubyでミニ言語処理系を作る

sonota88/vm2gol-v1 - GitHub


つっつきボイス:「少し前の記事です」「お〜、Rubyで言語処理系を作ってライフゲームまで動かしたんですね」「VMも自分で作ったのか」

参考: 言語処理系とは

「へ〜、この記事では便利なサポート系の外部ツールを使わずに、Rubyと最小限のgemだけで言語処理を書いてるんですね」「これはいい勉強になりそう」「こういうのは作ってて楽しいですよね」

「小さな言語処理系を一度自分で作ってみるのは大事」「そうそう」「難しすぎることをしてないのもポイントだと思います: リポジトリもシンプルですし、外部ツール群的なものも作っていませんね」「なるほど」

「外部ツールを作り始めると処理系全体がどんどん大きくなってしまいますけど、この記事はVMも純粋なVMにとどめていますし、コンパイラを作らずにコード生成機を直接書いてそこからアセンブリコードを出力しているようですね」「ふ〜む」「グラフィック部分も手作りしているとは面白い!」「外部ツールを作らずに、自分が掌握できる最小限の構成で処理系を作るというのがいいと思いました👍


「ところで、大学のコンピュータサイエンスの授業で使われていたような古典的な教科書に、よくこういう感じのミニ処理系の作り方が載ってましたね: LISPの教科書あたりでそういうのを見た覚えがちょっとあります」「そうそう、スタックマシンを作って動かすような言語処理系はよくこういう構成になりますよね」「オレオレチューリングマシン的なヤツですね🦾

参考: LISP - Wikipedia
参考: スタックマシン - Wikipedia
参考: チューリングマシン - Wikipedia

「自分で言語処理系を作るときは、とにかく最後まで作り切れるかどうかが最大の分かれ目」「そうそう、そこなんですよ」「小さくても言語系を作るのは根性要りますから」「でもそれを乗り越えて作り切れば、得るものは大きい」

「自分なら、ひとりでやるより人数集めて勉強会形式でやるかも」「自分はむしろひとりでやりたいかな」「お、こういうものこそ大学のゼミみたいな場の方がはかどりそうですけど?」「そういう面ももちろんあると思いますけど、こういうものを作るなら他の人の進捗を待たずに自分のペースでやりたい方なので」「それもわかります😆

🔗 method_missingの実用的な使い方3種(Ruby Weeklyより)


つっつきボイス:「Rubyのmethod_missingは、エラーハンドリング、メソッドのdelegation、DSL(Domain Specific Language: ドメイン固有言語)やライブラリで使える、まさにそのとおりですね」

参考: BasicObject#method_missing (Ruby 3.0.0 リファレンスマニュアル)
参考: ドメイン固有言語 - Wikipedia

🔗 method_missingよもやま話

「ところで、最近はRubyでDSLを作るいい教材ってあるのかな?以前はRubyでDSLを作るいい学習サイトがあったんですが、今は閉鎖してしまったんですよ」「ありゃ、残念」「そういう教材では必ずといっていいほどmethod_missingを使うテクニックが紹介されていますね」「『パーフェクトRuby』のような大きな書籍ならカバーされていると思いますが、RubyのDSLでのmethod_missingの使い方を手軽に学べる本もあるといいですよね」

「他の言語をやっていた人がRubyをやってみて驚くことのひとつが、このmethod_missingだろうなと思います」「たしかに!」「Rubyではこんなことができるのか!という一種の感動がありますね」


学習サイトの代わりに、method_missingとDSLについて書かれている英語記事をいくつか見繕いました。

参考: Writing a Domain-Specific Language in Ruby
参考: Metaprogramming: Writing in Ruby with... Ruby | Toptal

🔗 Rubyのgets


つっつきボイス:「意外に短い記事ですね」「Rubyの生のgetsに驚きの挙動ってあったかな?」

# 同記事より
#!/usr/bin/env ruby

puts "What is your name?"
your_name = gets.chomp
puts "Hi, #{your_name}!"

「上のコードを./gets.rb 123のように引数を渡して実行するとエラーになる、これは当然そうなりますよね」「ですよね、引数で渡したものをgetsで取れるというのは聞いたことないかも」「他の言語だとできたりするのかな?」

なお、以下は手元のRuby 3.0.0で上のコードを実行した結果です。

./gets.rb:4:in `gets': No such file or directory @ rb_sysopen - 123 (Errno::ENOENT)
    from ./gets.rb:4:in `gets'
    from ./gets.rb:4:in `<main>'

「Rubyの英語ドキュメント↓を見てみると…え?Kernel#getsにはARGV(Rubyスクリプト実行時に渡す引数の配列)を取る機能があるって書いてある!」「マジですか!?」「これは知らなかった」

参考: gets — Module: Kernel (Ruby 3.0.0)

Returns (and assigns to $_) the next line from the list of files in ARGV (or $*), or from standard input if no files are present on the command line. Returns nil at end of file.
ruby-doc.orgより

# ruby-doc.orgより
ARGV << "testfile"
print while gets

「日本語のドキュメントにも書かれてる↓」「ARGFはARGVをファイルとみなしたオブジェクトとある: ということはgetsにはファイル名を引数で渡せるのか!」「上でエラーになった123は、123という名前のファイルがないからエラーになったということなんですね」「やっと話が見えてきたかも」

参考: Kernel.#gets (Ruby 3.0.0 リファレンスマニュアル)

ARGFから一行読み込んで、それを返します。行の区切りは引数 rs で指定した文字列になります。
rs に nil を指定すると行区切りなしとみなしてファイルの内容をすべて読み込みます。ARGVに複数のファイル名が存在する場合は1度に1ファイルずつ読み込みます。空文字列 “” を指定すると連続する改行を行の区切りとみなします (パラグラフモード)。
読み込んだ文字列は組み込み変数 $_ にもセットされます。
docs.ruby-lang.orgより

「いや〜今までは記事にもあるようにずっと$stdin.getsでARGVを取ってましたけど↓、Kernel#getsでARGVを取れるなんて思いもよらなかった」「たしかに驚きですね」「getsメソッドのこの挙動にはちょっとびっくりしました」

# 同記事より
your_name = $stdin.gets.chomp

参考: argc,argvは何の略 | C言語のTipsとサンプル | C入門 基本情報対策講座のcClip

🔗 Rubyのプリントデバッグを便利にするライブラリ


つっつきボイス:「Matzがリツイートしているのを見かけたので取り上げてみました」「ricecreamというプリントデバッグに便利なgemを作った記事のようですね」

nodai2hITC/ricecream - GitHub

icは引数をそのまま戻り値として返してくれるから、式展開の中で使ったり以下のようにreturnに書いたりできるのが使いやすそう」「他にもいくつか機能があるようですね」

# 同記事より
return "result = #{ic foo.bar}"

「この機能がRuby本体に入ったら便利かも」「元記事にもありますけど、上と同じことはppでもできますね」「ちなみにpprequireしないと使えないんですよ(追記参照)」「Rubyのコンフィグでこういうデバッグ出力をオンオフできると便利かもしれませんね」「こういうツールは比較的作りやすいと思うので、自分で作ってみるのもいいと思います」

参考: library pp (Ruby 3.0.0 リファレンスマニュアル)
参考: class PP - Documentation for Ruby 3.0.0

「ところでデバッグプリントを消し忘れることってありませんか?」「それはコミット前に消さないといけませんよね」「おっしゃるとおり😅

追記(2020/02/10)

ppについて以下のご指摘をいただきました🙇。ありがとうございます!


「元記事を見ると、もともとPythonにicecreamという同じようなプリントデバッグツールがあるらしい↓」「Ruby版のicecreamだからricecreamと名付けたのかもしれませんね」「アイスクリームのアイコンがかわいい!」「sorbet↓のアイコンをちょっと思い出しました」「単にアイスクリームとシャーベットというモチーフが似ているだけだと思いますけどね」

gruns/icecream - GitHub
sorbet/sorbet - GitHub

🔗DB

🔗 Googleのsqlcommenter(Publickeyより)


つっつきボイス:「sqlcommenterはまだ使ったことがありませんが、Railsだと類似のgemが以前からいろいろありますね: ただ、他のフレームワークだとそういうツールがあるとも限らないので、そういうところでは便利なのかもしれないと思いました」

「sqlcommenterは、対応しているデータベースが比較的多いとか、OpenCensusのようなツールと組み合わせて使えたりするらしい」「いわゆるAPM(Application Performance Management)ツールでもそれに近いことができますね」「なるほど」「使いたい人が使うということでよいと思います」

参考: OpenCensus(OpenTelemetry)とは | フューチャー技術ブログ
参考: 【ツール8選】APMツールとは?基本解説やおすすめツールをご紹介! | QEEE

🔗クラウド/コンテナ/インフラ/Serverless

🔗 sudo脆弱性がmacOSにも影響


つっつきボイス:「sudo脆弱性は先週も間話題にしましたね(ウォッチ20210202)」「現時点ではAppleから情報はまだ出てないようです」

「Linuxのsudo脆弱性と似てるというだけで多少違いがありそうな感じ」「日本語記事にはそう出ていますね↓」

参考: sudoコマンドの脆弱性、「macOS」にも影響 - ZDNet Japan

「Linuxと違ってmacOSのバイナリはIntel版でもMach-Oバイナリですし、メモリマップなども違っているので、まったく同じ攻撃方法が使えるとはあまり思えませんが、sudoの脆弱性という視点から見て類似のものがMac版でも見つかった可能性ならありそう」「ふむふむ」「Macのsudoの脆弱性はソースコードレベルではたぶんLinuxのsudo脆弱性と違うだろうという気はしていますが、英語記事を眺めた感じでは、heap overflowにつながる可能性が書かれていたり、どうやらAPIレベルでは同じような結果になるらしい」

参考: Mach-O - Wikipedia
参考: Heap Overflow: Vulnerability and Heap Internals Explained - Infosec Resources

「これはAppleの対応を待つしかないでしょうね」「Windowsを使ってる人は大丈夫なんですね、いいな〜」

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 CSSのcontent-visibility

参考: content-visibility - CSS: カスケーディングスタイルシート | MDN

参考: CSS Containment - CSS: カスケーディングスタイルシート | MDN


つっつきボイス:「英語記事からの翻訳みたいなので、元記事を開いてみよう↓」

参考: content-visibility: the new CSS property that boosts your rendering performance


web.dev/content-visibilityより

「元記事にcodepenのリンクがありますね↓」「これでやってみましょうか」(一同でしばらく動かしてみる)

See the Pen
Content-visibility Demo: Base (Content Visibility on Grids)
by Una Kravets (@una)
on CodePen.

「Rerunボタンを押すといいのかな?」「動かしてみたけどまだピンとこない…」

「これかな?ちょっとわかりにくいですけど、codepenの表示を下にスクロールすると、途中でページが長くなったのが今スクロールバーに反映されたのが一瞬見えました」「え、今のがそうなんですか?」「Zoom越しだとうまく見えないかも…」「ではもう一度」「あ、ちょっと見えたかも」「下にスクロールすると何度かページの長さが変わっているように見えますね」

「CSSの末尾でcontent-visibility: auto;が指定されているから、この部分に効いてるんでしょうね」「あ、ここですか」

<!- https://codepen.io/una/pen/rNxEWLo より-->

.grid-3,
.grid-2,
.p-group-flex {
  content-visibility: auto;
}

「通常のWebページを開いてそのままにしていれば、JavaScriptでlazy loadingなどを行わない限り、ページが長くてもレンダリングはすべて終わっているものなので、普通ならスクロールしたときにページの縦の長さは変わらないはずですよね」「はい、そうですよね」「今codepenでやったように、レンダリングが終わっているはずのページを下にスクロールすると縦の長さが伸びたということは、ブラウザに表示されているページの中でそれまでビューポート↓に入っていなかった部分、つまりそれまでレンダリングされていなかった部分がレンダリングされたということなんでしょうね」「なるほど!」「ちょっと雰囲気がつかめてきたかも」「元記事を読まずに今試してみた限りでは、たぶんそうなんだろうと推測しました」「弊社のbabaさんのようなCSSに詳しい人に聞いてみたい」

参考: ビューポートの概念 - CSS: カスケーディングスタイルシート | MDN

「どうやらcontent-visibility: auto;は、ビューポートにさしかかるまでレンダリングを遅延するプロパティのようですね: これがlazy renderingのようなものだとすると、lazy loadingとは別物と考えるのがよさそうな気がしました」「なるほど、ちょっと腹落ちしました」「無条件に使うよりも、調べてから使う方がよさそう」「もう少しわかりやすい見せ方があるといいんですけどね」

auto
この要素は、レイアウトの封じ込め、スタイルの封じ込め、およびペイントの封じ込めをオンにします。要素がユーザーに関連していない場合は、その内容もスキップします。非表示とは異なり、スキップされたコンテンツは、ページ内検索、タブオーダーナビゲーションなどのユーザーエージェント機能に対して通常どおり利用可能である必要があり、通常どおりフォーカス可能で選択可能である必要があります。
MDN: content-visiblityより


後編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210208前編)Rails次期リリースがバージョン7に決定、thoughtbotのアプリケーションセキュリティガイドほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

Publickey

publickey_banner_captured

Ruby: Enumerableをreduceで徹底理解する#3 ソートとステート(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

参考: Enumerable#inject (Ruby 3.0.0 リファレンスマニュアル)reduceinjectのエイリアスです

Ruby: Enumerableをreduceで徹底理解する#3 ソートとステート(翻訳)

訳注

原文ではRubyのメソッドを「function」と表記しています。本文中でこれらを(おそらく通常のプリミティブなメソッドと区別する意味で)「高階関数」と呼んでいることから、それに従って本記事では原文に沿って「関数」と表記します。

Enumerable系関数をreduceで理解するうえで、もう少し説明の必要な関数がいくつかあります。最初の記事でハッシュやオブジェクトをreduceで理解したときのことを思い出して、ここでもやってみることにしましょう。

ソート

ここが楽しい部分です。Rubyはデフォルトでクイックソートアルゴリズムの変種を用いているので、私たちのコンパレータ(比較器)関数の理念に忠実に進めることにします。

sortsort_byはキーが少ない場合の速度が約10倍と著しく異なる点にご注意ください。sort_byが生成するタプルのセットは、コンパレータソートではなく単項関数ソートに使われます(Ruby 2.4.2 sort_by

手始めにシンプルなクイックソートをやってみましょう。

def sort(list)
  return [] if list.empty?

  head, *tail = list

  left  = tail.select { |i| i < head }
  right = tail.select { |i| i >= head }

  [*sort(left), head, *sort(right)]
end

sort([5,4,2,3,1])
# => [1, 2, 3, 4, 5]

原注: Rubyの*記号は、リストのコンテキストではsplat演算子になります。詳しくは以下の記事をどうぞ。
参考: Using splats to build up and tear apart arrays in Ruby

このクイックソートの原理は、要素リストの先頭を「ピボット」として選び、その要素より小さい要素のリストと、その要素と等しいかより大きい要素のリストに分割(partitioning)します。リストを2つに分割したら、それぞれについて同様にリストの分割を再帰的に繰り返すことで最終的にソート済みのリストを得られます。

しかし上の実装はどうも変な気がします。関数をコンパレータとして使っていませんし、再帰を何の工夫もなく使っているに過ぎません。もう少しマシなものにするにはどこから手を付けたらよいでしょうか?

最初に手を付けるのは、先ほども書いた「分割」の部分です。

def sort(list, &fn)
  fn ||= -> (a, b) { a <=> b }

  return [] if list.empty?

  head = list[0]

  left, center, right = list.reduce(
    Array.new(3) { [] }
  ) { |state, i|
    state[fn[i, head] + 1] << i
    state
  }

  [*sort(left, &fn), *center, *sort(right, &fn)]
end

sort([5,4,2,3,1])
# => [1, 2, 3, 4, 5]

原注: fn ||= -> {}を使う理由ですか?Rubyはデフォルトのブロックパラメータをあまり好まないので、少々裏技が必要になります。

ここにはいくつかの概念が導入されていますが、おそらく見慣れない方もいるかもしれません。最初の宇宙船演算子<=>は、コンパレータとして動作します。

参考: Ruby Spaceship <=> Operator

次はleft, center, rightに分けて代入していることです。これは、先頭の要素より小さいか、等しいか、大きいかを示す値を返す宇宙船演算子<=>と連動します。先頭の要素はコンパレータ関数のピボットとして使われます。<=>が-1から始まるので、きれいに分解できるように+1して0にします。

続いてArrayでブロックを作成しています。これは、要素が3つあるArrayがここで1つ欲しかったというだけのことです。

Array.new(3) { [] }

上述のleftを要素0、centerを要素1、rightを要素2にそれぞれ分割しています。それからleftの要素とrightの要素をソートして、splat演算子で配列の入れ子をflatten的に取り除きます。

単項ソート

上述のようにアリティ(arity: 引数の個数)が1のsort_by関数は低速です。しかしここにちょっぴり裏技を加えてみたらどうでしょう?Enumerableはreduceで実装できるのですから、ここでsort_byを自作してソートしてみましょう。

def sort_by(list, &fn)
  sort(list) { |a, b| fn[a] <=> fn[b] }
end

sort_by(%w[foo foobar baz bazzy]) { |w| w.length }
# => ["foo", "baz", "bazzy", "foobar"]

sort_by(%w[foo foobar baz bazzy]) { |w| -w.length }
# => ["foobar", "bazzy", "foo", "baz"]

できました!

minmax

今度はソートを実装したメソッドをいくつか見て結果を調べましょう。まともに書くとコード量が増えるので、上で定義したsortsort_byを流用することにします。

minfirst

minには、カウンタやコンパレータと同様にいくつの記法かあります。

def min(list, n = 1, &fn)
  fn ||= -> (a, b) { a <=> b }

  sorted = sort(list, &fn)
  (n > 1) ? sorted.first(n) : sorted.first
end

min([5,6,1, -10])
# => -10

min([5,6,1, -10], 2)
# => [-10, 1]

ケース1は、1個の最小値が欲しいだけの場合です。ケース2は値の配列を得られます。それではケース1にちょっぴり小技を効かせてみましょう。

def first(list, n = nil)
  return list[0] unless n

  list.reduce([]) { |a,i|
    break a if a.size == n
    a.push(i)
  }
end

first([1,2,3])
# => 1

first([1,2,3], 2)
# => [1, 2]

first([1,2,3], 4)
# => [1, 2, 3]

min_byは基本的にsort_byと同じ概念です。

def min_by(list, n = 1, &fn)
  min(list, n) { |a, b| fn[a] <=> fn[b] }
end

min_by([5,6,1, -10]) { |a| a**2 }
# => 1

min_by([5,6,1, -10], 2) { |a| a**2 }
# => [1, 5]

maxlast

maxは、minと逆である以外は同じです。

def max(list, n = 1, &fn)
  fn ||= -> (a, b) { a <=> b }

  sorted = sort(list, &fn)
  n > 1 ? sorted.last(n) : sorted.last
end

max([5,6,1, -10])
# => 6

max([5,6,1, -10], 2)
# => [5, 6]

最後のlastですが、これは少しばかりややこしい面があります。lastはRubyのうれしい機能ですが、関数型プログラミングとして完全ではないので、ここにも小技を効かせてみましょう。

def last(list, n = nil)
  return list[-1] unless n

  lasts = []

  list.reduce(0) { |i, _|
    break lasts if lasts.size == n
    lasts.push(list[-1 - i])
    i + 1
  }

  lasts
end

同じ要領でmax_byも以下のようにしてみます。

def max_by(list, n = 1, &fn)
  max(list, n) { |a, b| fn[a] <=> fn[b] }
end

max_by([5,6,1, -10]) { |a| a**2 }
# => -10

max_by([5,6,1, -10], 2) { |a| a**2 }
# => [6, -10]

minmaxstate

しかし、使いこなしが簡単とは言えない関数もいくつかあります。そのような関数で値をキャプチャしたいときは、コンテキストを多少渡してやる必要があります。

もちろん、外部の配列を使ってやる方法もあるのですが、それではコードが汚くなってしまいます。

手始めにハッシュでやってみましょう。

def minmax(list, &fn)
  fn ||= -> (a, b) { a <=> b }

  head, *tail = list
  result = tail.reduce({
    min: head,
    max: head
  }) { |state, i|
    min = fn[state[:min], i] > 0 ? i : state[:min]
    max = fn[state[:max], i] < 0 ? i : state[:max]

    {min: min, max: max}
  }

  [result[:min], result[:max]]
end

minmax([5,6,1, -10])
# => [-10, 6]

以前私が、reduceでは「何をreduceするか」をそれほど重要視しないと申し上げたことを思い出してみましょう。ではオブジェクトをreduceするとどうなるでしょうか?

MinMax = Struct.new(:min, :max)

def minmax(list, &fn)
  fn ||= -> (a, b) { a <=> b }

  head, *tail = list
  result = tail.reduce(MinMax.new(head, head)) { |state, i|
    min = fn[state.min, i] > 0 ? i : state.min
    max = fn[state.max, i] < 0 ? i : state.max

    MinMax.new(min, max)
  }

  [result.min, result.max]
end

minmax([5,6,1, -10])
# => [-10, 6]

def minmax_by(list, &fn)
  minmax(list) { |a, b| fn[a] <=> fn[b] }
end

minmax_by([5,6,1, -10]) { |a| a**2 }
# => [1, -10]

もちろん上で配列を使ってもよいのですが、このように考えることで可能性が大きく広がるところをぜひ想像してみてください。私は今回アキュムレータ(accumlator)と呼ばずにstateと呼んでいることにお気づきでしょうか?それには理由があるのですが、詳しく説明するにはもう少し準備が必要です。

まとめ:「もし可能だったら」

今回の記事をまとめる代わりに、本シリーズの最初の記事のまとめをおさらいすることにします。

「何もない(nothing)」または「何かがある(something)」というものを定義できたらどうなるでしょうか?

もしそれが可能であれば、map_selectや、その他のさまざまな操作の組み合わせの概念にどんな影響を与えるでしょうか?

関連記事

Ruby: Enumerableをreduceで徹底理解する#1 基本編(翻訳)

Ruby: Enumerableを`reduce`で徹底理解する#2 — No-OpとBoolean(翻訳)

Ruby 3のパターンマッチング応用(2)三目並べ(翻訳)

$
0
0

概要

概要

原著者の許諾を得て翻訳・公開いたします。

参考: 三目並べ - Wikipedia

Ruby 3のパターンマッチング応用(2)三目並べ(翻訳)

Ruby 3.0の目玉機能としてパターンマッチング(pattern matching)が導入されました。しかしパターンマッチングをどう使いこなせばよいのか、またパターンマッチングがどんなときに欲しくなるか、といった全貌がまだ見えていない方も大勢いらっしゃることでしょう。

今回は、パターンマッチングを「三目並べ」(Tic-Tac-Toe: 日本では「○×ゲーム」などとも呼ばれます)の勝ち判定に応用する方法をご紹介します。

今回のプログラム全体

最初に最終的なスクリプト全体をお目にかけます。その後で、個別のパートについて少しずつ見ていくことにします。

読み進めるうちに少々戸惑う部分もあるかと思いますが、何が行われているのかを頑張って読み取ってみましょう。

MOVE = /[XO]/.freeze

def board(*rows) = rows.map(&:chars)

def winner(board)
  case board
  in [
    [MOVE => move, ^move, ^move],
    [_, _, _],
    [_, _, _]
  ]
    [:horizontal, move]
  in [
    [_, _, _],
    [MOVE => move, ^move, ^move],
    [_, _, _]
  ]
    [:horizontal, move]
  in [
    [_, _, _],
    [_, _, _],
    [MOVE => move, ^move, ^move]
  ]
    [:horizontal, move]
  in [
    [MOVE => move, _, _],
    [^move, _, _],
    [^move, _, _]
  ]
    [:vertical, move]
  in [
    [_, MOVE => move, _],
    [_, ^move, _],
    [_, ^move, _]
  ]
    [:vertical, move]
  in [
    [_, _, MOVE => move],
    [_, _, ^move],
    [_, _, ^move]
  ]
    [:vertical, move]
  in [
    [MOVE => move, _, _],
    [_, ^move, _],
    [_, _, ^move]
  ]
    [:diagonal, move]
  in [
    [_, _, MOVE => move],
    [_, ^move, _],
    [^move, _, _]
  ]
    [:diagonal, move]
  else
    [:none, false]
  end
end

EXAMPLES = {
  straights: [
    # Win
    board('XXX', '   ', '   '),
    board('   ', 'OOO', '   '),
    board('   ', '   ', 'XXX'),

    # No Win
    board('X X', '   ', '   '),
    board('   ', 'O O', '   '),
    board('   ', '   ', 'X X'),
  ],

  verticals: [
    # Win
    board('X  ', 'X  ', 'X  '),
    board(' O ', ' O ', ' O '),
    board('  X', '  X', '  X'),

    # No Win
    board('   ', 'X  ', 'X  '),
    board(' O ', '   ', ' O '),
    board('  X', '  X', '   '),
  ],

  diagonals: [
    # Win
    board('O  ', ' O ', '  O'),
    board('  X', ' X ', 'X  '),

    # No Win
    board('O  ', ' O ', '   '),
    board('  X', ' X ', '   '),
  ]
}

EXAMPLES.each do |type, boards|
  boards.each do |board|
    puts "type: #{type}, win: #{winner(board)}"
  end
end

上のスクリプトにはさまざまなものが盛り込まれています。ここだけ読んで理解に不安があってもご心配なく。本記事でこの後詳しく解説いたします。

それでは深堀りを始めます。よろしいですか?

解説

石にマッチする正規表現

プログラムの冒頭は、XまたはOのいずれかにマッチする正規表現です。

MOVE = /[XO]/.freeze

freezeを追加している理由は、定数はfrozenにすべきだからです。さもないと本当の意味の定数ではなくなりますよね?

ゲーム盤

3×3のゲーム盤は、以下のように少々シンプルな方法で生成します。

def board(*rows) = rows.map(&:chars)

1つの行(row)は石の配置(move)を表します。

board('XXX', '   ', 'OO ')

原注: 本筋とは関係ありませんが、ここではto_sを表示に用いるまあまあのクラスを作りました。Boardクラスについて何かいいアイデアがありましたら、私のTwitter(@keystonelemur)までお知らせください。

これで以下のような2次元配列ができます。

[
  ['X', 'X', 'X'],
  [' ', ' ', ' '],
  ['O', 'O', ' ']
]

かなりそれっぽいゲーム盤に見えますが、私はirbやPryのようなREPL(Read-Eval-Print-Loop)でテストするのが常です。REPLならこうした実験を手っ取り早く行えるからです。

勝ちを判定する

ここからが本プログラムで面白くなってくる部分です。三目並べの解法はたくさんありますが、Rubyのパターンマッチングを応用すると問題解決方法に新しい視点を持ち込めます。

水平方向の勝ち判定

最初は「水平方向の勝ち判定」です。

case board
in [
  [MOVE => move, ^move, ^move],
  [_, _, _],
  [_, _, _]
]
  [:horizontal, move]
in [
  [_, _, _],
  [MOVE => move, ^move, ^move],
  [_, _, _]
]
  [:horizontal, move]
in [
  [_, _, _],
  [_, _, _],
  [MOVE => move, ^move, ^move]
]
  [:horizontal, move]

勝ち判定のパターンの中には、2種類の異なる行が含まれています。

最初のものは「値は何でもよい」ことを表します。

[_, _, _]

次のものはさらに興味深いものになっています。

[MOVE => move, ^move, ^move]

ここでは正規表現を用いて、石の配置(あるいは石の不在)が有効か無効かを判定しています。有効な場合は、=>記号(右矢印)を用いてmoveに値を代入します。なお=>記号はパターンマッチングで一般的に用いられます。

原注: 上のコードで正規表現が使えることを不思議に感じる方もいらっしゃるかもしれませんね。その理由は「Rubyのパターンマッチングではあらゆる値を===で比較する」からです。ここが重要です。===について詳しくは以下の記事をお読みになることをおすすめします。これが後ほど威力を発揮しますので、今は私を信じてください。
参考: Triple Equals Black Magic. For the most part, === is either… | by Brandon Weaver | Ruby Inside | Medium

その後、^moveを用いて値を呼び出し、同じ水平行にある残りの2つの値も同じであるという期待を記述します。水平行の最初(最も左)の値がXなら、残りの2つのXも同じ値、という具合です。

水平方向の3つのパターンのいずれかがマッチすれば、戻り値を得られます。

[:horizontal, move]

最初の3つのinの中でmoveに値が入れば勝ちが確定するので、勝者を返せるようになります。ここでは特にどのようにして勝ったかを知りたいので、勝った瞬間の手を最初の要素に含む「タプル的なArrayペア」を返します。

垂直方向の勝ち判定

垂直方向の勝ち判定は、水平方向の勝ち判定ととても似ていますが、チェックするカラムが垂直方向に並んでいる点だけが異なります。

in [
  [MOVE => move, _, _],
  [^move, _, _],
  [^move, _, _]
]
  [:vertical, move]
in [
  [_, MOVE => move, _],
  [_, ^move, _],
  [_, ^move, _]
]
  [:vertical, move]
in [
  [_, _, MOVE => move],
  [_, _, ^move],
  [_, _, ^move]
]
  [:vertical, move]

最初の例と同様、有効な配置の最初のキャプチャをmoveに代入してから、ピン演算子^でピン留めした^moveを用いて残りの2つのカラムの値も同じであることを確かめます。

その他の違いは、[:vertical, move]という戻り値で垂直方向の勝ちを示している点しかありません。

対角線方向の勝ち判定

対角線方向の勝ち判定はこれまでと見た目が少々異なりますが、考え方は同じです。

in [
  [MOVE => move, _, _],
  [_, ^move, _],
  [_, _, ^move]
]
  [:diagonal, move]
in [
  [_, _, MOVE => move],
  [_, ^move, _],
  [^move, _, _]
]
  [:diagonal, move]

ここは対角線上の値がすべて同じ配置になっているかどうかを知りたい箇所です。ここで勝ちが発生した場合は:diagonalを勝ちの戦略として返します。配置が斜めになっていると読みづらいので、いつもの私なら適度にスペース文字を入れてきれいに揃えるところですが、対角線上の勝ち判定についてはこのままにしておきます。

引き分け

Rubyのパターンマッチングは網羅的にチェックすることが期待できます。つまり、石がすべて置かれたのにどちらも勝たなかった場合をキャプチャするためのelseが必要ということです。ここでは以下のように書くことで引き分けの配置をすべてキャッチできます。

else
  [:none, false]
end

ここでは:noneを返して勝者がいなかったことを示し、勝ったときの配置の代わりにfalseを返します。

このelseを忘れると、引き分けのときに例外が発生してしまうのでよろしくありません。

本記事のサンプルは、前回のポーカーのときよりずっと読みやすいものになっています。

EXAMPLES = {
  straights: [
    # Win
    board('XXX', '   ', '   '),
    board('   ', 'OOO', '   '),
    board('   ', '   ', 'XXX'),

    # No Win
    board('X X', '   ', '   '),
    board('   ', 'O O', '   '),
    board('   ', '   ', 'X X'),
  ],

  verticals: [
    # Win
    board('X  ', 'X  ', 'X  '),
    board(' O ', ' O ', ' O '),
    board('  X', '  X', '  X'),

    # No Win
    board('   ', 'X  ', 'X  '),
    board(' O ', '   ', ' O '),
    board('  X', '  X', '   '),
  ],

  diagonals: [
    # Win
    board('O  ', ' O ', '  O'),
    board('  X', ' X ', 'X  '),

    # No Win
    board('O  ', ' O ', '   '),
    board('  X', ' X ', '   '),
  ]
}

コードを検証するため、いくつか勝ち条件や負け条件を並べてみたいと思います。こういうときにRSpecなどのテスティングツールをつい使いたくなりますが、ここでの目的はパターンマッチングを紹介することであって、テストそのものに深入りすることではありません。

rspec/rspec - GitHub

原注: ご興味のある方は元記事のコメント欄でリクエストいただければ、RSpecなどのテスティングツールを用いてテストする方法も追記したいと思います。

ここで重要なポイントは、エッジケースを見逃していないかどうかを確かめられるように、いくつかネガティブケース(マッチしないケース)を加えておくことです。

ここまで進んだら、以下のようにすべての例を実行して結果を表示します。

EXAMPLES.each do |type, boards|
  boards.each do |board|
    puts "type: #{type}, win: #{winner(board)}"
  end
end

まとめ

三目並べの解法は、ポーカーに比べて本質的にずっと簡単ですが、Ruby 3のある種のパターンマッチングのデモに使えます。本記事を皆さんが楽しんで学びを得られることを願っています。本ブログで取り上げて欲しいトピックが他にもありましたら、ぜひ私にお知らせください。

Ruby 3には楽しい機能が満載です。ときにはRuby 3を探検して楽しみましょう!

関連記事

Ruby 3のパターンマッチング応用(1)ポーカーゲーム(翻訳)

週刊Railsウォッチ(20210222)ActiveRecord::Relationの新メソッドload_asyncとexcluding、Active Jobのperform_laterの改善ほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsのなど最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下のコミットリストのChangelogを中心に見繕いました。

🔗 render 'partial'でのインスタンス変数代入が非推奨化された

Railsのパーシャルレンダリングでは、以下のようなインスタンス変数の代入が許されている。

render 'partial', :@name => "Maceo"

上はActionView::Base@nameを”Maceo”に設定する。
ユーザーが定義したインスタンス変数は代入が許されているのみならず、@_assigns@output_buffer@_config@_default_form_builderといったprivateなRails変数の上書きも可能になってしまっている。

この問題は当初Hackeroneに投稿され、@tenderloveがこの挙動をpublicで非推奨化することを決定した。
同PRより大意


つっつきボイス:「え、今までrender 'partial', :@name => "Maceo"みたいな書き方が可能だったの?」「自分もさっきrender 'partial'でインスタンス変数に:が付いているのを見てびっくりしました」「こんな書き方がシンタックスエラーにならなかったとは知らなかった…」「初めて見ました」「こういうふうに書くこと自体思い付かなかった」

🔗 Active Jobのperform_laterでジョブのenqueue失敗を処理できるようになった

perform_laterに、アダプタがジョブのenqueue試行後に実行されるブロックをオプションとして渡せるようになった。このブロックは、enqueueが成功しなかった場合であってもジョブのインスタンスを受け取る。
さらに、ActiveJobアダプタがActiveJob::EnqueueErrorエラーをraiseできるようになった。これはジョブインスタンス内でキャッチされて保存されるので、raiseされたEnqueueErrorをジョブのenqueueを試行するコードがブロックを用いてinspectできるようになる。

MyJob.perform_later do |job|
  unless job.successfully_enqueued?
    if job.enqueue_error&.message == "Redis was unavailable"
      # invoke some code that will retry the job after a delay
    end
  end
end

Daniel Morton
Changelogより大意


つっつきボイス:「perform_laterにブロックを渡してジョブのenqueueに失敗した時の処理を書けるようになったのか」「perform_laterブロックを渡す方法、Active Jobで発生するようになったEnqueueErrorを使う方法、successfully_enqueued
enqueue_errorプロパティを使う方法の3つが実装されたみたい」「テストコードを見ると3とおりともテストしてますね↓」

# activejob/test/cases/queuing_test.rb#42
  test "job is yielded to block after enqueue with successfully_enqueued property set" do
    HelloJob.perform_later "John" do |job|
      assert_equal "John says hello", JobBuffer.last_value
      assert_equal [ "John" ], job.arguments
      assert_equal true, job.successfully_enqueued?
      assert_nil job.enqueue_error
    end
  end

  test "when enqueuing raises an EnqueueError job is yielded to block with error set on job" do
    EnqueueErrorJob.perform_later do |job|
      assert_equal false, job.successfully_enqueued?
      assert_equal ActiveJob::EnqueueError, job.enqueue_error.class
    end
  end
# activejob/test/jobs/enqueue_error_job.rb#3
class EnqueueErrorJob < ActiveJob::Base
  class EnqueueErrorAdapter
    class << self
      def enqueue(*)
        raise ActiveJob::EnqueueError, "There was an error enqueuing the job"
      end

      def enqueue_at(*)
        raise ActiveJob::EnqueueError, "There was an error enqueuing the job"
      end
    end
  end

  self.queue_adapter = EnqueueErrorAdapter

  def perform
    raise "This should never be called"
  end
end

「Changelogのコード例にある”Redis was unavailable”みたいに、enqueueに失敗する可能性は常にあるので、EnqueueErrorは必要ですね👍」「このやり方覚えとこうっと」

「この機能が追加されたということは、これまでperform_laterでジョブのenqueueが失敗したときの公式な処理手段がなかったということなんでしょう」「今まではどうやって処理してたんでしょうね?」「変更前のperform_laterではenqueueを返してはいたけど↓、successfully_enqueuedenqueue_errorなどに相当するものがなかったので、enqueueが成功したかどうかを知る方法がなかったということなのかな?」「この感じだと公式な処理方法はこれまでなかったみたいですね」「従来はsuccessfully_enqueuedenqueue_errorなどに相当するものを自分で書いて処理するしかなかったんだろうと想像できます」

# activejob/lib/active_job/enqueuing.rb#L
      def perform_later(*args)
-       job_or_instantiate(*args).enqueue
+       job = job_or_instantiate(*args)
+       enqueue_result = job.enqueue
+
+       yield job if block_given?
+
+       enqueue_result
      end

🔗 ActiveRecord::Relationの新メソッド2つ: load_asyncexcluding

このメソッドは、スレッドプールから非同期実行されるようにクエリをスケジューリングする。
バックグラウンドスレッドがクエリを実行可能になる前にこの結果にアクセスすると、フォアグラウンドで実行されるようになる。
これは、結果が必要になるより前の実行時間が長いクエリや、独立したクエリを複数実行する必要のあるコントローラで有用。

def index
  @categories = Category.some_complex_scope.load_async
  @posts = Post.some_complex_scope.load_async
end

Jean Boussier
#41372 Changelogより大意


つっつきボイス:「1つ目はActiveRecord::Relation#load_asyncメソッドですか」「コードを見るとRubyのThreadで実装されていますね↓:将来もしかするとThreadの代わりにRubyのRactorを使うようになるのかもしれないとちょっと想像してみました」

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L460
      def schedule_query(future_result) # :nodoc:
        @async_executor.post { future_result.execute_or_skip }
+       Thread.pass
      end

参考: class Thread (Ruby 3.0.0 リファレンスマニュアル)
参考: ractor - Documentation for Ruby 3.0.0

「コントローラでlazy loadingする方法だとビューなどで実際に参照されるまで読み込みが開始されないんですが、このload_asyncならビューなどで参照される前に読み込みが開始されるので、期待どおりに動けばブラウザにレスポンスを早く返せるようにできるでしょうね」「お〜」

「ビューをレンダリングしている間にクエリを投げられるという感じでしょうか?」「ビューに限らず使えるでしょうね: たとえば以下のコードのCategory.some_complex_scopeが仮にやや遠くにあるサーバーへのクエリだとすると、ここでeager loadingもせずに普通にfindを使ってクエリを発行すると、結果が戻ってくるまでここでブロックされてしまうという問題がまず考えられます」「ふむふむ」

# Changelogより
def index
  @categories = Category.some_complex_scope.load_async
  @posts = Post.some_complex_scope.load_async
end

「同じ箇所でeager loadingする場合は、必要になった瞬間から読み込みが開始されますが、その場合もコードがブロックされるのは同じです」「たしかに」

「このload_asyncを使えば、そのクエリを実行するためだけのスレッドが新たに生成されるので、純粋な待ち合わせの時間は短縮されるでしょうね」「なるほど、コードがブロックされなくなるということですか」「重たくなりそうなクエリをバックグラウンドのスレッドで扱うというのはなかなかうまい方法だと思います👍」「言われてみると速くなりそうな気がしてきました」「load_asyncだとスレッド生成のコストも少しはかかると思いますが大きくはなさそう」「マルチプルデータベースにもいいかも」

「その代わり、別スレッドでエラーが発生した場合にどうやって拾うかは考えないといけないでしょうね」「あ、その場合どうすればいいんでしょうか?」「エラーそのものは別スレッド側で拾うしかないと思います」

load_asyncしたクエリはトランザクションが効かなくなりそうなので、使い方を間違えるとデッドロックするかも」「それもそうか」「たぶんデッドロックしそうな使い方はしないように、ということなんでしょうね」

参考: デッドロック - Wikipedia

「プルリクを見た感じでは、エラーの場合どうするかについては特に書かれてないのかな?」「お、トランザクションの中でload_asyncした場合のテストが書かれてますよ↓」「あ、ホントだ」

# activerecord/test/cases/relation/load_async_test.rb#55
    def test_load_async_from_transaction
      posts = nil
      Post.transaction do
        Post.where(author_id: 1).update_all(title: "In Transaction")
        posts = Post.where(author_id: 1).load_async
        assert_predicate posts, :scheduled?
        assert_predicate posts, :loaded?
        raise ActiveRecord::Rollback
      end

      assert_not_nil posts
      assert_equal ["In Transaction"], posts.map(&:title).uniq
    end

「上のテストコードを見ると、トランザクションの中で実行したPost.where(author_id: 1).load_asyncは、トランザクションが終わるまで待ってから動き出すようですね」「なるほど、このload_asyncではupdate_allが完了した後の結果が取れるのか」「完了まで待つのはRDBMS自体の機能です: 別スレッドがロックを掴んでいたら、タイムアウトするまでは待つ機能がRDBMSにあります」「なるほど」「いずれにしろトランザクションブロックの中ではload_asyncするかどうかで結果が変わる可能性があるので注意が必要でしょうね」

「たしかこのload_async的な機能は、これまで長らく待ち望まれていたけど実現していなかった機能だったと思います: 複数クエリを呼び出す処理をマルチスレッドで並列実行できるという意味で、その夢を実現する偉大なる一歩なんだろうなと思いました」「たしかに」


# 41439より
Post.excluding(post)
# SELECT "posts".* FROM "posts" WHERE "posts"."id" != 1

Post.excluding(post_one, post_two)
# SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2)

# And on associations (also supports collections as above).
post.comments.excluding(comment)
# SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" != 2

「2つめのexcludingはプルリクに書かれているとおりPost.where.not(id: post.id)のショートハンドですね↓」「NOT INに相当するのか」「where.notを書かなくていいのがよさそう」「where.notをたくさん書くときにはいいでしょうね」

This is short-hand for Post.where.not(id: post.id) (for a single record) and Post.where.not(id: [post_one.id, post_two.id]) (for a collection).

「その後で@kamipoさんがエイリアスも追加していました↓」「なるほど、excludingwithoutでも書けるんですね」

🔗 データベースアダプタの平均値集計を修正

:averageで呼ばれるActiveRecord::Calculations.calculateActiveRecord::Calculations.averageのエイリアス)が、カラムベースのtype castingを使うようになった。これによって、浮動小数点値(float)のカラムがFloatとして集計され、固定小数点値(decimal)のカラムがBigDecimalとして集計されるようになった。
整数は特殊ケースとして、常にBigDecimalとして扱われる(これは既にそうなっている)。
この変更前は、Railsのデータベースアダプタで平均値集計のときにto_dを呼んでいた。これは今後行われなくなる。この種のマジックに依存していた場合は、独自のActiveRecord::Typeを登録する必要がある(ActiveRecord::Attributes::ClassMethodsのドキュメントを参照)。
Changelogより抜粋


つっつきボイス:「decimal型のカラムをaverageメソッドで集計した際、スキーマ側で定義された型を無視してfloatに変換されて返ってくる問題を修正したということみたい」「テストを見ると、integerは常にBigDecimalで返すべきとなっている↓」「floatはFloatで、decimalはBigDecimalで返すべきということか」

# activerecord/test/cases/calculations_test.rb#50
  def test_should_return_decimal_average_of_integer_field
    value = Account.average(:id)
+
    assert_equal 3.5, value
+   assert_instance_of BigDecimal, value
  end
# activerecord/test/cases/calculations_test.rb#L64
  def test_should_return_float_average_if_db_returns_such
    NumericData.create!(temperature: 37.5)
-
    value = NumericData.average(:temperature)
-   assert_instance_of Float, value
+
    assert_equal 37.5, value
+   assert_instance_of Float, value
+ end
+
+ def test_should_return_decimal_average_if_db_returns_such
+   NumericData.create!(bank_balance: 37.50)
+   value = NumericData.average(:bank_balance)
+
+   assert_equal 37.50, value
+   assert_instance_of BigDecimal, value
  end

「あれ、37.5と37.50って型が違うんですか?」「お?テスト用の定義はスキーマのどこかにあるかな(しばらく探す): あった↓」「temperatureがfloatでbank_balanceがdecimalなのか、なるほど理解しました」「このスキーマ定義の型に応じてtype castingできているかどうかのテストということですね」

# rails/activerecord/test/schema/schema.rb#L651
  create_table :numeric_data, force: true do |t|
    t.decimal :bank_balance, precision: 10, scale: 2
    t.decimal :big_bank_balance, precision: 15, scale: 2
    t.decimal :unscaled_bank_balance, precision: 10
    t.decimal :world_population, precision: 20, scale: 0
    t.decimal :my_house_population, precision: 2, scale: 0
    t.decimal :decimal_number
    t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78
    t.numeric :numeric_number
    t.float   :temperature
    t.decimal :decimal_number_big_precision, precision: 20
    # Oracle/SQLServer supports precision up to 38
    if current_adapter?(:OracleAdapter, :SQLServerAdapter)
      t.decimal :atoms_in_universe, precision: 38, scale: 0
    else
      t.decimal :atoms_in_universe, precision: 55, scale: 0
    end
  end

「以下がその前にマージされたプルリクですね↓」

🔗Rails

🔗 Railsにマイクロサービスを追加する(Ruby Weeklyより)


つっつきボイス:「Railsにマイクロサービスですか?」「Railsをマイクロサービス化するんじゃなくて?」

# 同記事より
class QuizController < ApplicationController
  # Other controller methods omitted here
  # This is our new service method
  def participants
    response = {}
    response["participants"] = Attempt.all.map { |attempt| attempt.taker }
    render :json => JSON[response]
  end
end

「え、QuizController.action(:participants)でコントローラのアクションを直接呼び出してるのか↓、しかもpumaの引数で.ruファイルを指定してる」「すごい書き方ですね😳

# 同記事より
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run QuizController.action(:participants)
# 同記事より
bundle exec puma -b tcp://0.0.0.0:8080 config_svc_participant.ru

「これってマイクロサービスなんでしょうか?」「自分たちが考えているマイクロサービスとは設計思想が違いますけど、単機能のサービスという意味での『マイクロなサービス』と考えることも一応できるでしょうね」「なるほど、そういう意味ですか」「unbundled seriesと銘打たれている記事だから、いろいろ自由に試しているのかなと想像してみました」


「ところでこの記事、よくみるとEngine Yardのブログなんですね」「Engine YardのWebサイトってもっと赤っぽい色彩だった覚えがあるんですけど、随分青みがかってる」「Engine YardはHerokuと同じぐらい歴史がありますけど、今はRails以外もいろいろサポートするようになっているので、シンボルカラーをRailsを連想する赤から変えたのかもしれませんね」「あ、そうかも」「そういえば、昔の赤色の時代のEngine Yardのステッカーがそこに転がっているパソコンに貼ってありますよ」

🔗 Railsのcycleメソッド(Ruby Weeklyより)


つっつきボイス:「i % 2 == 0を避ける、とは?」

<!-- 同記事より -->
<% @foods.each_with_index do |food, i| %>
  <tr class="<%= i % 2 == 0 ? 'bg-gray-200' : 'bg-gray-100' %>">
    <td><%= food %></td>
  </tr>
<% end %>

「なるほど、以下のようにeach_with_indexの中でcycleビューヘルパーを使うと、上みたいに1行ずつ交互に背景色を変えたりするときにi % 2 == 0 ?で偶数奇数を判定したりせずに書けるのね↓」「へ〜!」「Railsにこんなヘルパーもあるとは」

<!-- 同記事より -->
<% @foods.each_with_index do |food, i| %>
  <tr class="<%= cycle('bg-gray-200', 'bg-gray-100') %>">
    <td><%= food %></td>
  </tr>
<% end %>

odd?even?で書くよりもcycleの方が意味的にも自然かも」「背景色に限らず、ループの中で何かを順繰りに表示したいときに幅広く使えそうですね👍

🔗Ruby

🔗 reductionとは何か、FiberがRubyコンカレンシーの解である理由(Ruby Weeklyより)


つっつきボイス:「お、まだこの記事の図しか見てませんが、こんなふうに↓スレッドを丁寧に図で説明しているのがすごくよさそう」「おぉ!」


同記事より

「Fiberだけじゃなくて最後の方でRactorまで説明してくれている」「こうやって時系列に沿った図になっているのがうれしいですね↓」「マルチスレッド系の解説って、こういう図がないと読んでてつらいんですよ」


同記事より

「歯ごたえありそうだけど、この記事は読む価値がありそうな予感がします👍」「週末に頑張って読んでみようかな」

🔗 M1 MacでRailsアプリを開発するときのコツ(Ruby Weeklyより)


つっつきボイス:「Rails discussionがRuby Weeklyで紹介されていました」「Appleシリコンだ」「Dockerもアップデートされて、ようやくいくつかの機能がM1チップでまともに動くようになったという記事も今日見かけましたね」

参考: Apple M1チップ対応のDocker Desktop、同梱のKubernetesも実行可能に - Publickey

「次に買うMacBookはM1チップにしようかな」「たぶん今後はM1チップのしか買えなくなると思いますよ」「え、そうなんですか?」「AppleがIntelへのCPUの新規発注を絞り込んでいるらしいという話もあるので、おそらく現行のIntel Macはいったんディスコンになる流れなんじゃないかなと予想しています」「あ〜なるほど」「最近Intelがこういう広告↓を打っているのが話題になってますけど、そのあたりも理由じゃないかと言われていますね」

参考: インテル、「MacにできないことがPCにできる」キャンペーンを展開 - Engadget 日本版


「ところで、最近一部で話題になったcpu-monkey.comのベンチマーク↓は、スペック情報は参考になる部分もあるんですけど、シングルコアでM1とM1Xの差がほとんどないとか、自分も含めてこのベンチマークの信憑性にはちょっと疑問があるんですよ」「あ、そうなんですか」

参考: Apple M1X vs. Apple M1 - Benchmark and Specs

「ここに載ってたスペックを見ると、M1Xにはメモリが32GB搭載できるらしいので次に買ってみてもいいかなと思うんですが、M1XのTDP(Thermal Design Power)が35Wもあるのでバッテリーの持ちという面では優位性がちょっと下がるかもと思っているところです」「それでも32GB積めたらいいですよね」「今の自分の使い方だと16GBではもうやっていけないレベルです」「今のMacBookは最大16GBですもんね」(以下延々)

参考: TDP - CPU の選び方


今回は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210209後編)Rubyでミニ言語処理系を作る、Kernel#getsの意外な機能、CSSのcontent-visibilityほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

Ruby 3.0でアドベント問題集を解く(2日目-1)パスワードの理念(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

今回の出題: Day 2: Password Philosophy - Advent of Code 2020

Ruby 3.0でアドベント問題集を解く(2日目-1)パスワードの理念(翻訳)

この問題では、いくつかのパスワードを指定の基準で検証する必要があります。

1-3 a: abcde

2つの数字(訳注: 13)は、右端のパスワードに含まれるべき文字の数を示しています。上記の場合、abcde の中には a が1〜3回出現しているはずであり、それに該当すれば有効なパスワードです。

しかし以下のパスワードは、bが出現していないので無効です。

1-3 b: cdefg

以上を踏まえたうえで、以下の私の完全な解答例を追ってみましょう。

正規表現の短期養成講座

欲しいデータを得るための最も理想的な方法は、おそらく正規表現を使ってデータを解析することでしょう。その前に、いくつかの概念について簡単に説明し、 Rubularというサイトで実際に動かして確かめてみましょう。

完全なチュートリアルではなくクイックリファレンスですが、Rubyの正規表現についてひととおりのチュートリアルが欲しい場合は以下をご覧ください。

参考: Ruby regular expressions - working with regular expressions in Ruby

複数箇所にマッチさせる

言うまでもなく、正規表現の中にはリテラルシンボル(訳注: 通常の文字)も書けますが、「任意の文字(.)」「任意の数値(\d)」「小文字アルファベット([a-z])」「リテラルスペース文字(\s)」などを指定したい場合もあります。これらは入力形式の記述に使えるので、入力から必要なものを抽出できます。

また、直前の文字がゼロ回以上繰り返されることを表す*や、直前の文字が1回以上繰り返されることを表す+といったモディファイア(modifier: 修飾語)も使えます。

'abc'.match(/[a-z]+/)
# => #<MatchData "abc">

'012'.match(/[a-z]+/)
# => nil

グループをキャプチャする

私たちが使いたい正規表現の主な機能のひとつは、「入力の特定の部分をキャプチャして名前を付ける」という概念です。たとえば、以下のようにIPアドレスを(素朴に)解析したいとしましょう。

IP_REGEX = /(?<first>\d+)\.(?<second>\d+)\.(?<third>\d+)\.(?<fourth>\d+)/

'192.168.1.1'.match(IP_REGEX)
# => #<MatchData "192.168.1.1"
#   first:"192"
#   second:"168"
#   third:"1"
#   fourth:"1"
# >

実際にRubularで動かすと4つのグループが表示されますが、この(?<capture_name>captured_regex_value)では「抽出したい部分に名前を付けられる」という概念を用いています。以下のようにnamed_captures を使えばマッチデータからこれらの値を取得することも可能ですが、マッチしなかったときに nil が返る場合は、&.(訳注: Rubyの「ぼっち演算子」)を付けておくことをおすすめします。

'192.168.1.1'.match(IP_REGEX).named_captures
# => {"first"=>"192", "second"=>"168", "third"=>"1", "fourth"=>"1"}

ホワイトスペースの有無を区別しないようにする

正規表現の末尾にオプションを追加すると、動作を変更できます。たとえば、以下のようにx を付けるとホワイトスペース(訳注: 半角スペースとタブと改行の総称)を区別しなくなるので複数行を扱えますし、コメントを追加して正規表現の意図をわかりやすくすることもできます。

IP_REGEX = /
  (?<first>\d+)\.
  (?<second>\d+)\.
  (?<third>\d+)\.
  (?<fourth>\d+)
/x

考え方は同じですが、このように書くことで何が行われているかを読み取るのがずっと簡単になります。

正規表現を適用する

以上を踏まえると、以下のような正規表現になります。

PASSWORD_INFO = /
  # 行の冒頭
  ^

  # 入力全体を「input」でキャプチャする
  (?<input>

    # 文字のローカウントを取得して「low_count」でキャプチャする
    (?<low_count>\d+)

    # ダッシュ記号を無視する
    -

    # ハイカウントをキャプチャする
    (?<high_count>\d+)

    #リテラルスペース
    \s

    # 対象となる文字を検索する
    (?<target_letter>[a-z]):

    \s

    # 行の残りの部分がパスワードになる
    (?<password>[a-z]+)

  # 入力終了
  )

  # 行終了
  $
/x

ここでやりたいのは、「行全体」と「カウント、ターゲット文字、パスワード本体などの部分」を両方取りたいということです。上の正規表現を以下の2つの例にそれぞれ適用すると、以下の結果が得られます。

'1-3 a: abcde'.match(PASSWORD_INFO)
# => #<MatchData "1-3 a: abcde"
#   input:"1-3 a: abcde"
#   low_count:"1"
#   high_count:"3"
#   target_letter:"a"
#   password:"abcde"
# >

'1-3 b: cdefg'.match(PASSWORD_INFO)
# => #<MatchData "1-3 b: cdefg"
#   input:"1-3 b: cdefg"
#   low_count:"1"
#   high_count:"3"
#   target_letter:"b"
#   password:"cdefg"
# >

どうやらこれで問題を解けそうです。ただし取得したカウントは数値ではなく文字列であることに注意しましょう。

パスワードを抽出する

以下のようなパスワード抽出用ワンライナーを作ってみましょう。

def extract_password(line) =
    line.match(PASSWORD_INFO)&.named_captures&.transform_keys(&:to_sym)

これは技術的にはワンライナーではなく、どことなくPython風に見えます。手短に説明すると、Rubyのendレスメソッドには式または値を1つしか含めるべきではありません(2行目のチェインが式です)。

ホワイトスペースも自由に含められますので、今後コードをもう一度読み返したときに、ホワイトスペースで読みやすくしておいてよかったと思えることでしょう。

ところで、ここで唯一新しい部分はtransform_keysです。transform_keysは、すべてのStringキーをSymbolに変換します。その理由は、次のセクションでパターンマッチの可能性を示すためです。

文字を種類ごとに数える

ワンライナーは手っ取り早く機能を使うのに便利ですが、最終的にはパスワードの中にある1文字1文字の数を数えてくれるものも必要そうです。

原注: もっと効率的な方法はもちろんありますが、読者の演習課題としておきます。

ありがたいことに、Rubyのtallyメソッドはこんなときにうってつけです。

def letter_counts(word) = word.chars.tally

tallyは、コレクション内にある項目ごとのカウントを返します。

letter_counts 'aabbccc'
# => {"a"=>2, "b"=>2, "c"=>3}

tallyを使わないとしたら以下のようになっていたでしょう。

def letter_counts(word) =
  word.chars.each_with_object(Hash.new(0)) { |c, counts| counts[c] += 1 }

私はtallyが好きで割とよく使っています。以下の記事にも書いたように、DavidとShannonとStephがこのメソッドに命名するときに私もお手伝いいたしました。

参考: Ruby 2.7 — Enumerable#tally. Enumerable#tally is a new function… | by Brandon Weaver | Medium

有効なパスワード

これで、パスワードが有効かどうかの確認に必要なツールが揃ったので、関数自体を見てみましょう。

def valid_passwords(input)
  input.filter_map do
    extracted_password = extract_password(_1) or next
    extracted_password => {
      input:, low_count:, high_count:, target_letter:, password:
    }

    low_count, high_count = low_count.to_i, high_count.to_i
    count = letter_counts(password)[target_letter]

    input if (low_count..high_count).include?(count)
  end
end

何やらいろいろやっていますが、順に見ていくことにします。

filter_map

filter_mapはその名のとおり、filtermapを一度に行える楽しいメソッドです。コードでは、正しい要素だけを結果に残すのに使っています。

[1, 2, 3].filter_map { _1 * 2 if _1.even? }
# => 4

ここでは必ずしもfilter_mapを使わなくても、selectでできます。しかし「パスワードそのものの抽出」「マッチしたデータの保持」「フィルタによる無効な入力の除外」をまとめて行いたい場合は本当に便利です。

orってもしかしてPerlの?

はい、私は必要であれば英語のor演算子を使います。今回はまさにそうしたケースです。抽出したパスワードがnilであれば早期に脱出してよいことがわかります。以下のorはPerl由来の「早期復帰」や「例外処理」テクニックで、Rubyにも取り入れられています。

line = gets or raise 'error!'

以前も述べましたが、私は「左から右に読み下せる」ことを特に重視しているので、業務用コードではorandの利用を避けています。

右代入ロケット演算子=>によるワンライナーパターンマッチング

ここがお楽しみの部分です。

// JavaScript
{ a, b } = { a: 1, b: 2 }
// => a = 1, b = 2

以下は、上のJavaScriptの分割代入に似ていますが、代入の向きが逆です。これは右代入(RHA: Right Hand Assignment)と呼ばれています。

# Ruby
extracted_password => {
 input:, low_count:, high_count:, target_letter:, password:
}

キーをシンボルに割り当てていた理由は、この機能をお見せするためだったのです。これが デフォルトでObject#sendに実装されていればよかったのにとしみじみ思います。もしそうなっていたら、以下のように書けたことでしょう。

person_object => { name:, age: }

残念ながらそうはいきませんでしたが。しかし、このパターンには「===に応答するバリデーションも含まれる」のがうれしい点です。===の面白さについては以下の記事をご覧ください。

参考: Triple Equals Black Magic. For the most part, === is either… | by Brandon Weaver | Ruby Inside | Medium

それでは=>の利用例を見ていきましょう。

{ a: 1, b: 2, c: 'foo' } => { a: Integer, b: 2, c: }

注意!これは実験的機能につき、微妙なバグや妙な挙動が少々あります。たとえば、上はローカル変数cしか定義されません。ローカル変数を3つとも定義するには以下のように書く必要があります。

{ a: 1, b: 2, c: 'foo' } => {
  a: Integer => a, b: 2 => b, c:
}

これはバグだと思いますので、そのうちレポートするつもりです。

残りを駆け足で解説

訳注

原文見出し「The Rest of the Owl」は、直接的には「フクロウの絵の残り(を最後まで描いてください)」という意味ですが、チュートリアルなどで詳しく作業を説明せずに実践するときの定番の言い回しです。

参考: Draw the rest of the owl now : funny

この関数の他の部分では、さほど珍しいことはやっていません。

low_count, high_count = low_count.to_i, high_count.to_i
count = letter_counts(password)[target_letter]

input if (low_count..high_count).include?(count)

ここではカウントの指定と実際のカウントを比較したいと思います。単語単語内にあるそれぞれの文字カウントを取得して、それが期待の範囲に収まるかどうかをチェックします。

文字カウントが期待の範囲内に収まる場合はinputを返し、そうでない場合は戻り値から除外されます。

有効なパスワードカウント

他のアドベント問題と同じように、上の関数を別の関数でラップすることで有効なパスワードのカウントを取得できるようになります。

def valid_password_count(...) = valid_passwords(...).size

繰り返しますが、私はこのように2つのアイデアを切り離しておくことでデバッグしやすくする方法がとても気に入っています。

入力を読み取る

これでAdvent of Codeで出題された入力を解析できるようになります。

File.readlines(ARGV[0]).then { puts valid_password_count(_1) }

この行は、今後Advent of Codeの問題を解くときに大きく変わることはないでしょう。

(その2に続く)

関連記事

はじめての正規表現とベストプラクティス1: 基本となる8つの正規表現

週刊Railsウォッチ(20210301前編)Rails 6.1.3がリリース、Active Supportのbefore?とafter?、link_to_unless_currentほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストのChangelogを中心に見繕いました。

🔗 ActionController::Live::Buffer#writelnが追加

DHH自らによるプルリクです。

# 同PRより
send_stream(filename: "subscribers.csv") do |stream|
  stream.writeln "email_address,updated_at"

  @subscribers.find_each do |subscriber|
    stream.writeln [ subscriber.email_address, subscriber.updated_at ].join(",")
  end
end

つっつきボイス:「末尾で改行されるwritelnができたんですね」「名前がどことなくJavaなどのprintln風味かも」「Javaやったことないです…」「JavaやってたのもJava 5の頃なのであまり思い出せませんが」「私も同じぐらいJava忘れてますね😆

参考: Javaで文字列を出力する:print(), println() | UX MILK

writelnend_with?を使って末尾の改行が重複しないように処理してくれている↓」「これは賢い👍

# actionpack/lib/action_controller/metal/live.rb#166
      # Same as +write+ but automatically include a newline at the end of the string.
      def writeln(string)
        write string.end_with?("\n") ? string : "#{string}\n"
      end

🔗 to_strが使えるオブジェクトもredirect_toに渡せるようになった

Addressable::URIのように)#to_strが使えるものなら何でもredirect_toのlocationとして渡せるようにした。
ojab
Changelogより大意


つっつきボイス:「Addressable::URIって何だろうと思ったら、どうやら外部のgemらしい↓」

sporkmonger/addressable - GitHub

to_strって普段使わないけど、これってto_sとどう違うんだっけ?」「to_strto_sの違いをググったらTechRacho記事が出てきた↓」「自分も同じ記事を見てます😆」「to_sto_iみたいな短い変換メソッドは明示的な変換、to_strto_intみたいな名前の長い変換メソッドは暗黙的な変換、だそうです」「う〜んまだ違いがピンとこない」

Rubyの明示的/暗黙的な型変換についてのメモ(翻訳)

「記事を見ると、文字列を式展開しないで直接"string" + otherと結合すると、otherto_strが呼ばれるのか」「へ〜、otherで呼ばれるのはto_sじゃないんですね!」「式展開ならto_sが呼ばれます↓」

Rubyでの文字列連結に「#+」ではなく式展開「#{}」を使うべき理由

「Rubyのドキュメントも見ると、to_strは『文字列が使われるすべての場面で代置可能』『それ自体が文字列とみなせるもの』のときにだけ定義する、と書かれてる」「to_strが定義されているということは、文字列とみなしてよいものということなのかな」

参考: Object#to_str (Ruby 3.0.0 リファレンスマニュアル)

オブジェクトの String への暗黙の変換が必要なときに内部で呼ばれます。デフォルトでは定義されていません。
説明のためここに記載してありますが、このメソッドは実際には Object クラスには定義されていません。必要に応じてサブクラスで定義すべきものです。
このメソッドを定義する条件は、

  • 文字列が使われるすべての場面で代置可能であるような、
  • 文字列そのものとみなせるようなもの

という厳しいものになっています。
docs.ruby-lang.orgより

to_strは定義されているとは限らないけど、to_s↓はObjectに実装されているからすべてのオブジェクトに対して使えますね」

参考: Object#to_s (Ruby 3.0.0 リファレンスマニュアル)

「元コードは、when節のRegexp#===比較が暗黙型変換(to_str)で評価されるのにwhen節内でoptionsをそのまま返してしまっていたため、_compute_redirect_to_locationの戻り値がStringにならないケースがあったのを、#41390では明示的にoptions.to_strすることで戻り値がStringであることを保証するようにしたんですね↓」

# actionpack/lib/action_controller/metal/redirecting.rb#101
    def _compute_redirect_to_location(request, options) #:nodoc:
      case options
...
      when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
-       options
+       options.to_str
      when String
        request.protocol + request.host_with_port + options
      when Proc
        _compute_redirect_to_location request, instance_eval(&options)
      else
        url_for(options)
      end.delete("\0\r\n")
    end

🔗 fixtureのhas_many :throughでタイムスタンプを設定するようになった

has_many :through関連付けのfixtureがjoin tableでタイムスタンプを設定するようになった。
以下のfixtureがあるとする。

### monkeys.yml
george:
  name: George the Monkey
  fruits: apple
### fruits.yml
apple:
  name: apple

このjoin table (fruit_monkeys)がcreated_atupdated_atを含む場合、fixtureの読み込み時に展開されるようになった。従来はこれらのカラムをrequireするとクラッシュし、しない場合はnullのままになった。
Alex Ghiculescu
同Changelogより大意


つっつきボイス:「fixtureで中間テーブルにタイムスタンプ定義がある場合にcreated_atupdated_atが自動設定されずにnullになっていたのが、このプルリクで修正されたようです」「お〜」「修正されたのはactive_record_fixture_set/のfixtureですね↓」「中間テーブルにタイムスタンプを付けることはありうるので、修正されてよかった」

# activerecord/lib/active_record/fixture_set/table_row.rb#L37
+       def timestamp_column_names
+         ModelMetadata.new(@association.through_reflection.klass).timestamp_column_names
+       end
      end
...

        def add_join_records(association)
          # This is the case when the join table has no fixtures file
          if (targets = @row.delete(association.name.to_s))
            table_name  = association.join_table
            column_type = association.primary_key_type
            lhs_key     = association.lhs_key
            rhs_key     = association.rhs_key

            targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
            joins   = targets.map do |target|
              { lhs_key => @row[model_metadata.primary_key_name],
                rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
-             join = { lhs_key => @row[model_metadata.primary_key_name],
-                      rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
+             association.timestamp_column_names.each do |col|
+               join[col] = @now
+             end
+             join
+           end
+           @table_rows.tables[table_name].concat(joins)
          end

Rails API: `ActiveRecord::FixtureSet`(翻訳)

🔗 ActiveSupport::CurrentAttributesのキーワード引数を修正

最近のRubyで(キーワード引数周りが)変更された後にキーワード引数を引き続き使いたい人向けのシンプルな改良。
respond_to?変更は、RSpecでパーシャルのダブル(double)をチェックするときのCurrentAttributesのスタブ化の問題を解決するのが目的。
キーワード引数が動かないという前提を置く理由がないので、ドキュメントは変更していない。
同PRより大意


つっつきボイス:「CurrentAttributesmethod_missingでキーワード引数を中継できるように**kwargsが追加されてますね↓」

# activesupport/lib/active_support/current_attributes.rb#L158
-       def method_missing(name, *args, &block)
+       def method_missing(name, *args, **kwargs, &block)
          # Caches the method definition as a singleton method of the receiver.
          #
          # By letting #delegate handle it, we avoid an enclosure that'll capture args.
          singleton_class.delegate name, to: :instance

-         send(name, *args, &block)
+         send(name, *args, **kwargs, &block)
+       end
+
+       def respond_to_missing?(name, _)
+         super || instance.respond_to?(name)
+       end

「今は、受け取った引数をすべて受け取ってdelegateできるようにするには、上のようにmethod_missing(name, *args, **kwargs, &block)と書くのか」「へ〜、今はこうなんですね」「これはnameが必ず存在する前提ですが、そういうのがない場合は*args, **kwargs, &blockと書けばすべての引数を受け取れるんでしょうね」

🔗 ドキュメント更新: redirect_toの危険な利用法について


つっつきボイス:「APIドキュメントとガイドの更新です↓」「redirect_toにユーザー入力をそのまま渡すのは一般に危険、たしかに」「これはもうおっしゃるとおりとしか言いようがない」「そんなことをする人がいるんだろうかと思いますが、たぶんいたから明示的にドキュメントとガイドにも書くことにしたんでしょうね」

# guides/source/security.md#L345
-If it is at the end of the URL it will hardly be noticed and redirects the user to the attacker.com host. A simple countermeasure would be to _include only the expected parameters in a legacy action_ (again a permitted list approach, as opposed to removing unexpected parameters). _And if you redirect to a URL, check it with a permitted list or a regular expression_.
+If it is at the end of the URL it will hardly be noticed and redirects the user to the `attacker.com` host. As a general rule, passing user input directly into `redirect_to` is considered dangerous. A simple countermeasure would be to _include only the expected parameters in a legacy action_ (again a permitted list approach, as opposed to removing unexpected parameters). _And if you redirect to a URL, check it with a permitted list or a regular expression_.

🔗Rails

🔗 @kamipoさん記事より


つっつきボイス:「MySQL 8.0のクライアントでMySQL 5.7のサーバーに接続、これやっちゃってました😅」「記事を見た感じではMySQLの接続のハンドシェイクでの問題なので、接続した後でSET NAMESコマンドを渡して設定する分には大丈夫そうですね: ちょうど記事にも書いてあった↓」「お〜」

一応、接続後にSET NAMES utf8mb4すればサーバー側のutf8mb4のdefault collationが設定されるが、最悪のケースをカバーするために適切に設定してるひとには必要ない処理が増えて損をすることになるのでなんとか回避したい気持ちがあるけど、現状はそういう感じ。
同記事より

「元々この41403↓で上がってたissueを解決しようとしていたんですね」

「そうそう、記事にもあるように、MySQL 8.0.1からデフォルトのコレーションがutf8mb4_general_ciからutf8mb4_0900_ai_ciに変わっているんですよ↓: 後者はMySQL 5.7の頃にはなかったから認識されなくて、MySQL 5.7サーバーのデフォルトのコレーションにフォールバックしてたのか」「え、そこ変わっちゃってたんですか!」「MySQL 5.7サーバーには新しいutf8mb4_0900_ai_ciid:255がないから、MySQL 8.0クライアントからこれを渡しても認識しようがありませんね」「このことを知らなかったら原因を見つけるのは難しそう…」

ここで表題の “MySQL 8.0のクライアントでMySQL 5.7のサーバーに接続するとcharsetが設定されないかもしれない” についてなんですが、MySQL 8.0.1からutf8mb4のdefault collationがutf8mb4_general_ci (id: 45)からutf8mb4_0900_ai_ci (id: 255)に変更されたため、MySQL 8.0のクライアントがuff8mb4でサーバーに接続するとid: 255のcs_numberを送るけどMySQL 5.7はid: 255のcs_numberを知らないのでサーバー側のデフォルトの設定が採用されるという仕組み。
同記事より

🔗 AS句で作ったカラムにDBの型情報はない


つっつきボイス:「永和システムマネジメントさんのブログです」「『AS句で作ったカラムにDBの型情報はない』、そういうカラムはスキーマに定義がありませんのでそのとおりですね」

「ただ、これはActive Recordがどこまでよしなにマジックを効かせてくれるかという流れを何となくでも把握していないと、すぐには見当がつかないと思います」「あ、そういうことですか」「Active Recordが内部的にSHOW FIELDS(MySQLの場合)を使ってスキーマの型情報を取得していることを理解していれば、AS句で取ったカラムに型情報がないことは推測できると思います」「ふむふむ」「SHOW FIELDSはテーブルに対して実行するものなんですが、テーブルがなければSHOW FIELDSを実行できないので、AS句でSELECTした結果に対してはSHOW FIELDSできません」「なるほど」

「もしやりたければ、VIEW(データベースビュー)↓を作ればやれますよ: VIEWならSHOW FIELDSが使えるので」「あ〜、なるほど」

RDBMSのVIEWを使ってRailsのデータアクセスをいい感じにする【銀座Rails#10】

参考: ビュー (データベース) - Wikipedia

「記事にも、スキーマにないカラムの型はデフォルトでActiveModel::Type::Valueになると書かれていますね↓」

その過程で、AS 句で作ったカラムに型情報がつかない理由もわかります。 スキーマにないカラムはデフォルトで ActiveModel::Type::Value 型になるから、です。
同記事より

「今つっつきの場にいませんが、たしかBPS Webチームのkazz氏が、AS句で組み立てたクエリに何らかの方法で型ヒントを渡してカラムの型を認識させるというのをやっていた覚えがありますので、何か方法はあると思います」「お〜、やれそうなんですね」「でなければVIEWを使うか、さもなければAS句を使わないことでしょうね」

「Active Recordがあまりにいろんなことをよしなにやってくれるので、その内部構造に興味を持ってない人はこういうところでハマるでしょうね」「ですね、自分はハマる自信あります😅

🔗 Dateの比較で<>を混同しないようにする


つっつきボイス:「Boring Railsっていう名前が面白い」「Date同士を比較するときには<とか>よりもActive Supportのbefore?after?の方が便利だよという話のようですね」「この辺は自分もよく不安になるので動かして確かめてます」

# 同記事
start_date = Date.new(2019, 3, 31)
end_date = Date.new(2019, 4, 1)

start_date.before? end_date
#=> true
end_date.after? start_date
#=> true

start_date = Date.new(2020, 8, 11)
end_date = Date.new(2018, 8, 11)

start_date.before? end_date
#=> false

「英語圏的にはbefore?after?の方がわかりやすいのかな?」「記事のコード例だと、start_dateが2019, 3, 31で、end_dateが2019, 4, 1だと、start_date.before? end_dateはtrueになる」「えっ、falseになるのかなという気がしましたけど?」「パッと見に逆かと思っちゃいますよね: 先行するstart_dateが主語で、それに対してend_datebefore?と読まないといけないんでしょうね」「あ、それでちょっとわかってきたかも」「いつものようなオブジェクト指向的な語順で読まずに、普通の関数のような語順で読むとよさそう」「before?after?はRails 6で追加されたんですね↓」

参考: Rails 6 adds before? and after? to Date and Time | BigBinary Blog


「そういえば、このboringrails.comの他の記事にも反応を見つけました↓」「link_to_unless_currentって見たことなかった」

「なるほど、この図↓のようにWebページのメニューなどで現在のページだけリンクを生成しないで、それ以外のリンクを有効にできるメソッドなのか」「あ、それちょっと便利かも😋」「こんなメソッドあるの知らなかった〜」


同記事より

参考: link_to_unless_current — ActionView::Helpers::UrlHelper

🔗 Rails 6.1.3がリリース


つっつきボイス:「お、Rails 6.1.3が出たんですね」「リリース情報見落としててRuby Weeklyの見出しで知りました🙇」「今回の更新は少なそう」


前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210222)ActiveRecord::Relationの新メソッドload_asyncとexcluding、Active Jobのperform_laterの改善ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

Ruby 3.0でアドベント問題集を解く(2日目-2)パスワードの別の理念(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

今回の出題: Day 2: Password Philosophy - Advent of Code 2020

本記事は以下の記事の続編です。

Ruby 3.0でアドベント問題集を解く(2日目-1)パスワードの理念(翻訳)

Ruby 3.0でアドベント問題集を解く(2日目-2)パスワードの別の理念(翻訳)

あらゆるプログラミングと同様、仕様には変更がつきものです。ここでは、パスワードが有効であることを解析する方法が変更されます。

(訳注: 2つの)数値は、実際にはパスワードの位置を表します。ここでは、パスワードの位置を表す数値の片方だけに目的の文字が存在し、かつ両方には存在していないことをチェックしたいと思います。

# 以下は有効(位置1にaがあり、位置3にはaがない)
1-3 a: abcde

# 以下は無効(位置1にも位置3にもbがない)
1-3 b: cdefg

# # 以下は無効(位置2にも位置9にもcがある)
2-9 c: ccccccccc

これに対応するには、前回の関数を若干変更することになります。

正規表現を変更する

意図が変更されたので、キャプチャグループ名をlow_counthigh_countからposition_oneposition_twoにそれぞれ変更する必要があります。

PASSWORD_INFO = /
  # 行の冒頭
  ^

  # 入力全体を「input」でキャプチャする
  (?<input>

    # 1番目の位置を取得する
    (?<position_one>\d+)

    # ダッシュは無視する
    -

    # 続いて2番目の位置を取得する
    (?<position_two>\d+)

    # リテラルスペース
    \s

    # 対象となる文字を検索する
    (?<target_letter>[a-z]):

    \s

    # 行の残りの部分がパスワードになる
    (?<password>[a-z]+)

  # 入力終了
  )

  # 行終了
  $
/x

変更はわずかですが、命名は重要なので名前もしっかり変更しておきましょう。

有効なパスワード

それでは、メインの関数でどこを変更する必要があるかを見てみましょう。

def valid_passwords(input)
  input.filter_map do
    extracted_password = extract_password(_1) or next

    extracted_password => {
      input:, position_one:, position_two:, target_letter:, password:
    }

    position_one = position_one.to_i - 1
    position_two = position_two.to_i - 1

    char_one, char_two = password[position_one], password[position_two]

    input if [char_one, char_two].one?(target_letter)
  end
end

パターンマッチング用の名前

ここでは、low_counthigh_countを、位置を表す新しい名前に変更する必要があります。

extracted_password => {
  input:, position_one:, position_two:, target_letter:, password:
}

添字にLuaを使っている人がいるのかな?

今回の仕様では、添字(index)に0ではなく1を使うよう指示されているので、位置にオフセットを与えて補正する必要があります。

position_one = position_one.to_i - 1
position_two = position_two.to_i - 1

訳注

参考: 配列とテーブル - Dolphin TAS制作@wiki - atwiki(アットウィキ) — Lua言語ではテーブルの添字は1から始まります。

文字を取り出す

続いて、それらの位置にある文字をそれぞれ取得したいと思います。

char_one, char_two = password[position_one], password[position_two]

「ワンライナーで多重代入を使うと顰蹙を買うでしょうか?」おそらく。「この多重代入は動くのでしょうか?」はい。

バリデーション

ここでは、数値をチェックするのではなく、2つの文字の片方「だけ」が該当していることを確認したいと思います。

input if [char_one, char_two].one?(target_letter)

上のコードは、上手く別の表現を使っている(訳注: Arrayone?メソッドを使って処理している)以外は、排他的論理和(XOR)として知られた概念です。しかしRubyでは以下のように^記号でXORを表現できます。

true ^ true
# => false

true ^ false
# => true

false ^ true
# => true

false ^ false
# => false

ただし^を使う場合は演算子の優先順位に注意しましょう。以下のように書くとうまくいきません。

input if char_one == target_letter ^ char_two == target_letter

(訳注: ^は)||(OR)と&&(AND)と挙動が異なるので、これはもしかするとバグではないかと個人的に感じていますが、理由についてはわかりません。ともあれ、上の^は以下のように()で囲むことで修正できます。

input if (char_one == target_letter) ^ (char_two == target_letter)

関数のその他の部分はそのままでよいので、これで要件を満たせるようになりました。

訳注

参考: 演算子式 (Ruby 3.0.0 リファレンスマニュアル) — 演算子の優先順位

2日目のまとめ

2日目はこれで終わりですが、今後数日ないしは数週間かけてそれぞれの問題に取り組み、解決策や方法論を探っていきたいと思います。

私のオリジナル解答をすべてご覧になりたい方は、完全なコメント付きの以下のリポジトリをどうぞ。

baweaver/advent_of_code_2020 - GitHub

関連記事

Ruby 3.0でアドベント問題集を解く(2日目-1)パスワードの理念(翻訳)


週刊Railsウォッチ(20210303後編)Bundlerのセキュリティ修正、Rubyのガベージコレクション記事、Rubyが24歳にほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 Bundlerのセキュリティ修正(Ruby Weeklyより)


つっつきボイス:「Bundlerサイトのブログです」「Bundlerをよりセキュアに、ですか」

「なるほど、複数のgemサーバーを使っている状況でgemの名前がコンフリクトする場合に、以下のようにどのサーバーから取ってくるかを明示的に指定できるようになったんですね」「おぉ?」「たとえばmy-private-gemがrubygems.orgにもmy-private-serverにも置かれている場合、従来だとどちらからフェッチするかを指定できなかったのが、以下のようにsourceブロックで明示的に囲んで取得元を指定できるようになった」「あ、そういうことですか」

# Gemfile

source "https://rubygems.org"

source "https://my-private-server" do
  gem "my-private-gem"
end

「記事で引用されている”dependency confusion”↓という問題が従来のBundlerにもあったんですね」

参考: Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies | by Alex Birsan | Feb, 2021 | Medium

「上のdependency confusion記事は他の言語の話ですが、この図↓のnpm dependenciesの記述みたいに、自分が直接取得元を指定していない間接的なdependency confusionについては、たしかに従来のBundlerのgem依存関係でも起きる可能性がありますね: Bundlerがこういう問題に対処可能になったのはいいことだと思います👍」


Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companiesより


「お、同じ記事のこの図が、この問題を使った攻撃の手口を端的に表してますね: 複数リポジトリを使う場合、同じ名前でバージョン番号が大きいライブラリをリポジトリに置かれてしまうとそちらを参照してしまうことがあります」

🔗 必要がない限りインスタンス変数をattr_readerで公開するのは止めよう


つっつきボイス:「記事冒頭のコード例↓の2つ目でattr_reader: nameを書いている理由は、末尾の"#{name.upcase}!!!"name@を付けたくないからということらしい」「リファクタリングがしやすくなるからとかタイポに強くなるからとか理由が書かれていますね」

# 同記事より: 元のバージョン
class User
  def initialize(name)
    @name = name
  end

  def loud_name
    "#{@name.upcase}!!!"
  end
end
# 同記事より: attr_readerバージョン
class User
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def loud_name
    "#{name.upcase}!!!"
  end
end

「でもそのためにattr_reader@nameを不必要にpublicにしてしまうのはよくない、やるならせめて以下のようなprivateなattr_readerにしましょうと: たしかに」「言われてみればそうですね」「著者としてはそれもあまり好きでないようですね」

# 同記事より: private化バージョン
class User
  def initialize(name)
    @name = name
  end

  def loud_name
    "#{name.upcase}!!!"
  end

  private

  attr_reader :name
end

「ところで、name@を付けたくない理由が他にあるとすれば、@nameだと変更できてしまうからでしょうね」「あ、たしかに」「attr_readerにすれば=で代入できなくなるので、その部分の安全度は上がります」「うっかり=で代入しても大丈夫になりますね」「あまり意識的に使ったことはありませんが、こんなふうにattr_readerをprivateにするテクニックは以前どこかで見たことがある気がします」


後で探すと、attr_readerを用いてインスタンス変数を隠蔽する方法はSandi Metz『オブジェクト指向設計実践ガイド(“Practical Object-Oriented Design in Ruby”)』に書かれていると以下の記事で知りました。私が持っている同書日本語PDF版(初版第1刷)で確認するとp46〜47にありました。以下の記事には、attr_readerprivateにする方法についても紹介されています。

参考: Rubyのインスタンス変数の直接参照について - 雑草SEの備忘録

🔗 Rubyのガベージコレクションを深掘りする(Ruby Weeklyより)


つっつきボイス:「Rubyのガベージコレクション解説記事です」「Rubyで使われているというtri-color mark and sweepってどこかで聞いたかも」


同記事より

後で探すとWikipediaにTri-color markingの項がありました↓。

参考: Tri-color marking — Tracing garbage collection - Wikipedia

「Rubyのガベージコレクション周りについてはRubyKaigiでよく発表されているので、そのあたりを探してみるといいかも」「そういえばAaron PattersonさんやNate BerkopecなどもRubyのガベージコレクション記事執筆や発表をよく行っていますね↓」

Rubyのヒープをビジュアル表示する(翻訳)

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)

🔗 その他Ruby

つっつきボイス:「Rubyの誕生日って1993年の2/24なのか」「2/24って昨日じゃないですか(注: つっつき時点)」「Rubyという名前が付けられた日が誕生日なんですね」「Ruby作者自らが発信した情報で知ることができるのはいいですね」

「そういえば昨年JavaScriptも25歳になったらしいですね↓」「Rubyが1歳、いや2歳若い!」

🔗クラウド/コンテナ/インフラ/Serverless

🔗 AWS CodeDeploy


つっつきボイス:「今日Webチーム内発表のお題になっていたことでCodeDeployを知りました」「AWSのCodeDeployって見たことなかったんですけど使われているんですか?」「お、普通に使われていますよ」

「AWS CodeDeployのいいところは、ALB配下で複数台冗長化している際にシステム全体を正常にゼロダウンタイムでデプロイするための機能が統合されている点ですね: ALBに設定されたリクエストタイムアウト値に合わせた待ち時間でインスタンス切り離しを行い、healthcheckに合わせた設定でサービスインするという、当たり前だけどロールバックなどを考慮すると地味にめんどくさい機能がAWSサービスとしてまるっと統合されている良さがあります」「そういうのを自動でやってくれるんですか」「そういう機能がビルトインされています: たぶんですけどAuto ScalingでオートスケールするものならCodeDeployを使うのが便利だと思いますよ」「へ〜」

参考: CodeDeploy と Elastic Load Balancing の統合 - AWS CodeDeploy
参考: Integrating CodeDeploy with Amazon EC2 Auto Scaling - AWS CodeDeploy

「デプロイではインスタンスをALBからいきなり切り離したらダメなんですよ: まずリクエストの流入を止める、次に処理中のリクエストがあるかもしれないのでタイムアウトまで待つ、そしてトラフィックが完全に来なくなったことを確認してからインスタンスを切り離してデプロイして、終わったらつなぎ直す、もし途中で失敗したらロールバックする」「ふむふむ」「そうした一連の処理をCodeDeployはひととおりやってくれるんですよ」「そこまでやってくれるんですね」「今度やってみようかな」


「ちなみにBPSの自分のWebチームでは、AWSに全部まとめたいときなどにAWS CodeDeployが結構使われています」「お〜、そうでしたか」「数で言うとたぶんGitHub ActionsよりAWS CodeDeployの方が多いかも」「そんなに!」

GitLab自社運用のための注意点とノウハウ(2018/06版)

「自分はCIをGitHub Actionsに乗せようかなと思っているところなんですよ」「GitHubでやっているならGitHub Actionsがいいでしょうね: BPSの場合はGitLabを使っているのでプロジェクトによってはGitLab CIを使うことがよくあります」

参考: GitLabの継続的インテグレーションと継続的デリバリー | GitLab.JP


後編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210301前編)Rails 6.1.3がリリース、Active Supportのbefore?とafter?、link_to_unless_currentほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

DB Weekly

db_weekly_banner

Rails: present?より便利なActiveSupportのpresenceメソッド(翻訳)

$
0
0

更新情報

  • 2018/09/27: 初版公開
  • 2021/02/26: 細部を更新

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしています。

Rails: present?より便利なActiveSupportのpresenceメソッド(翻訳)

Active SupportはRubyのコアライブラリにメソッドをたくさん追加するので、何かと非難が集中します。特に評判がよろしくないのは、RubyのObjectクラスへのパッチです。

RubyのあらゆるオブジェクトはObjectのサブクラスなので、Objectクラスにメソッドを追加すればコードのあらゆるオブジェクトにそのメソッドが追加されることになります。

Active Supportでの拡張に関するドキュメントでもう少し詳しく見てみましょう。

そうしたメソッドのひとつが#presenceです。これはお馴染みの#blank?(訳注: #empty?のエイリアス)や#present?に比べて馴染みの薄いメソッドです。

次のように書くのではなく

変数の値を表示するのに、次のように長ったらしい条件を使う。

class User < ApplicationRecord
  validates :email, presence: true

  def friendly_name
    if nickname.present?
      nickname
    elsif given_name.present?
      given_name
    else
      email.split('@').first
    end
  end
end

次のように書く

Active Supportの#presenceメソッドを使う

class User < ApplicationRecord
  validates :email, presence: true

  def friendly_name
    nickname.presence || given_name.presence || email_local_part
  end

  private

  def email_local_part
    email.split('@').first
  end
end

そうする理由

#presenceメソッドは、オブジェクトが存在すればそのオブジェクトを返し、存在しなければnilを返したい場合にとても便利なショートカットです。

このメソッドは、Railsのビューでデータが存在するかどうかをチェクする部分でよく見かけるobject.present? ? object : nilという書き方と同等です。

このメソッドは、文字列や配列が空の場合にも有用なソリューションです。空の場合には#presencenilを返します。

そうしない理由があるとすれば

#presenceメソッドはRailsでしか利用できません。Rubyだけを使う場合、このメソッドのためだけにActive Supportをインクルードするほどの価値はおそらくないでしょう。

Railsを使う場合でも、既存のRubyクラスを拡張するこうしたRailsの習慣に抵抗を覚えるのも無理もないかもしれません。

自分のコードでモンキーパッチを使って既存クラスを改変する場合、標準のRubyクラスが思わぬ振る舞いを示すときにバグを踏みやすくなります。この種のコーディングスタイルで悩ましいのは、主にこうした点です。

モンキーパッチによる落とし穴が心配になってしまうと、たとえRailsが当てるパッチであっても避けたい気持ちになるかもしれません。

Railsのライブラリセットはしっかりメンテされていて広く用いられているので、私はこうしたパッチは安全とみなして、コードをきれいに書ける方を選びます。

関連記事

Rails: ActiveRecordのスコープで`present?`を使うとパフォーマンスが落ちることがある(翻訳)

Rails: pluckでメモリを大幅に節約する(翻訳)

週刊Railsウォッチ(20210308前編)書籍『Ruby on Rails Performance Apocrypha』、rswag gemほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストのChangelogを中心に見繕いました。今週はやや少なめです。

🔗 Active Storageで画像を生成できない場合にPreviewErrorをraiseするようになった

プレビューを生成できない場合、キャプチャされるIOストリームが空になり、0バイトのプレビューファイルが生成されてActive Storageサービスに保存されてしまう。
これが発生した理由は、PopplerがいくつかのPDFプレビュー生成に失敗して0バイトのファイルになってしまったことによるもの。これらの”preview”をリサイズするとMiniMagickでエラーが発生していた。MiniMagickのこのエラーは、0バイトのファイルに対して試みられた場合には正しい結果に思えるが、プレビューをキャプチャしようとした子プロセスが失敗して終了した場合にPreviewerが通常どおり処理を進めるのは正しいとは考えにくい。
そこで、previewerの子プロセスが0以外のステータスで終了した場合は例外を発生するようにした。
同コミットより大意


つっつきボイス:「今までは画像リサイズ時にMiniMagickのエラーが出ていたのに、そのまま空のプレビューファイルをストレージに置いてしまってたんですね」「プレビュー生成はいろんな理由で失敗する可能性があるので、Active Storageとして明示的にエラーを出すようになったのはいいですね👍

「ライブラリが返す例外の詳細メッセージに生のコマンドが含まれていたりすると、攻撃者がそれを元にライブラリのバージョンなどを推測して手がかりにする可能性がなくもないので、そういう意味でもライブラリの例外をそのまま出さないようにしておくのは一見地味ですが結構重要だと思います」「なるほど」

🔗 Action Textのリッチテキストでto_trix_htmlが使われるようになった

このプルリクの目的は、rich_text_area_tagを使う場合はAction Textのto_sではなくto_trix_htmlを使うようにすること。to_trix_htmlメソッドをto_sto_plain_textと同じようにもっと使いやすくするためにモデルに委譲を追加した。
同PRより大意


つっつきボイス:「Action Textで使われているTrixは、Basecampが開発したWebアプリ向けリッチテキストエディタ↓」「to_trix_htmlが前からあったのでそれを使うようにしたようですね」

basecamp/trix - GitHub

🔗 Arel::CrudArel::Tableから削除した

compile_updatecompile_deleteは、元もArel::Tableではまったく動かなかった(Arel::Table@ast@ctxもないため)。
compile_insertcreate_insertは動くが、レシーバーの情報をまったく使っていないので、代わりにArel::InsertManager.new(arel_table)を使うこと。
同PRより大意


つっつきボイス:「これは文字通り、実は動かないメソッドを削除したのか」

# activerecord/lib/arel/table.rb#L3
module Arel # :nodoc: all
  class Table
-   include Arel::Crud
    include Arel::FactoryMethods
    include Arel::AliasPredication

🔗 すべてのtree managerでイニシャライザがテーブルを受け取れるようになった

SelectManager80ad95bテーブルを受け取るようになったが、他のmanagerはクエリ生成で必須の場合でもそうなっていない。
SelectManagerと同様、他のtree managerもすべてテーブルを受け取っていいと思う。
同PRより大意


つっつきボイス:「tree managerって初めて見ました」「お、Arelの中にマネージャーがありますね↓」「ホントだ」「いかにもSQLのSELECT/INSERT/UPDATE/DELETEに対応する感じ」

80ad95bからたどっていくとvisitorsがあった↓」「名前や処理からしてVisitorパターンで実装されているようですね」

参考: rails/visitors.rb at main · rails/rails

    def to_dot
      collector = Arel::Collectors::PlainString.new
      collector = Visitors::Dot.new.accept @ast, collector
      collector.value
    end

    def to_sql(engine = Table.engine)
      collector = Arel::Collectors::SQLString.new
      collector = engine.connection.visitor.accept @ast, collector
      collector.value
    end

参考: Visitor パターン - Wikipedia

[保存版]人間が読んで理解できるデザインパターン解説#3: 振舞い系(翻訳)

🔗 excludingの修正


つっつきボイス:「この間追加されたexcludingに修正が入ったそうです(ウォッチ20210222)」「そういえばwithoutというエイリアスもexcludingに追加されてましたね」

「なるほど、1つ目は引数が1個以上必要だったのをなしでもよいことにしたのか↓: プルリクメッセージにも『Active Recordは引数がなくても1=1(つまりtrue)を追加するようになっている』とあるので、それなら引数なしでもよいと判断したんでしょうね」「なるほど」「引数があるかどうかをチェックする分岐を書かなくて済むのはうれしい👍

# activerecord/lib/active_record/relation/query_methods.rb#L1131
    def excluding(*records)
      records.flatten!(1)

-     raise ArgumentError, "You must pass at least one #{klass.name} object to #excluding." if records.empty?
-
      if records.any? { |record| !record.is_a?(klass) }
        raise ArgumentError, "You must only pass a single or collection of #{klass.name} objects to #excluding."
      end
      spawn.excluding!(records)
    end

「2つ目は引数がnilの場合に対応したとありますね」「これも実質上と同じ話ですね: Active Recordはnilが渡された場合に1=1(つまりtrue)を追加するようになっているとプルリクに書かれてます」

🔗Rails

🔗 電子書籍『Ruby on Rails Performance Apocrypha』


つっつきボイス:「TechRacho記事でもお世話になっているNate Berkopecさんは”The Complete Guide to Rails Performance”という本↓を以前から出していますが、先ごろ新たに”Ruby on Rails Performance Apocrypha”という別冊的な本を出したそうです」

以下はつっつき後に見つけたツイートです。”The Complete Guide to Rails Performance”はRails 6とRuby 2.7にも対応したそうです。

「トピックを見ると、GVLとかworker killerのような、Railsを長く触っているとどこかで遭遇しそうなパフォーマンス周りの話題を扱ってますね↓」「この本は面白そうな予感がします👍

  • Benchmarks for Rails Apps
  • Reading Flamegraphs
  • Microservices and Trends
  • Why is Ruby Slow?
  • Popularity
  • Page Weights and Frontend Load Times
  • What is the GVL?
  • Reproducing Issues Locally
  • Worker Killers
  • Multi-threading
  • Read Replicas

Rubyのスケール時にGVLの特性を効果的に活用する(翻訳)


「アポクリファって?」「こんな意味でした↓」「なるほど、外典ですか」「アポクリファっていかにもゲームのタイトルとかにありそうな響き」

apocrypha: 外伝、外典(ギリシャ語のἀπόκρυφα(複数形: 隠されたもの)由来で、cryptに通じる)

後で調べると、ゲームを元にした小説のタイトルが見つかりました↓。

参考: Fate/Apocrypha - Wikipedia

🔗 Redisベースのrate limiter(Ruby Weeklyより)


つっつきボイス:「ざっと見た感じでは、rate control用のパラメータをRedisに保存して更新・参照することで、APIサーバーのクライアントTokenごとのrate制御を実現しているようですね」

# 同記事より
class RateLimiter
  TimedOut = ::Class.new(::StandardError)

  REDIS_KEY = "harmonogram_#{Rails.env}_rate_limiter_lock".freeze

  def initialize(redis = Redis.current)
    @redis = redis
    @interval = 1 # seconds between subsequent calls
    @timeout = 15 # amount of time to wait for a time slot
  end

  def with_limited_rate
    started_at = Time.now
    retries = 0

    until claim_time_slot!
      if Time.now - timeout > started_at
        raise TimedOut, "Started at: #{started_at}, timeout: #{timeout}, retries: #{retries}"
      end

      sleep seconds_until_next_slot(retries += 1)
    end

    yield
  end

  private

  attr_reader :redis, :interval, :timeout

redis/redis - GitHub

「こういった処理はRedisでなくてもできますが、マルチスレッドな環境でもアトミックにアクセスできる高速な共有ストレージとしてRedisを使ったんでしょうね」「ふむふむ」「Redisのアトミックアクセス用命令を使うのかなと思ったら、RedisのPTTLというms単位で減っていくTTLカウンタを使うことで、clientへの割り当てタイムスロットの残時間をRedisで管理させているみたい」

## 同記事より
def seconds_until_next_slot(retries)
  ttl = redis.pttl(REDIS_KEY)
  ttl = ttl.negative? ? interval * 1000 : ttl
  ttl += calculate_next_slot_offset(retries)
  ttl / 1000.0
end

# Calculates an offset between 10ms and 50ms to avoid hitting the key right before it expires.
# As the number of retries grows, the offset gets smaller to prioritize earlier requests.
def calculate_next_slot_offset(retries)
  [10, 50 - [retries, 50].min].max
end

参考: PTTL – Redis

「RDBだけだとこういうときに困るのでRedisが欲しくなってきますね」

「あと、こういう処理をRDBで行うとDB接続のコネクションプールを使ってしまうので、Redisのような別のストレージを使う方がありがたい面はあります」「なるほど」

🔗 rswag: RSpecからSwagger JSONを生成(Ruby Weeklyより)

rswag/rswag - GitHub


つっつきボイス:「rswagは見たことなかったけど、★は1000件超えていますね」「rspecのrequest spec風DSLを書いて、Swaggerに対応した定義ファイルの出力やAPIテストができるgemだそうです」

参考: API Documentation & Design Tools for Teams | Swagger

「こんなふうにRSpecにSwagger的なspecを書ける↓、そしてそれを元にrake rswag:specs:swaggerizeでSwagger用のJSONを生成できるのか、へ〜!」

# 同リポジトリより
# spec/integration/blogs_spec.rb
require 'swagger_helper'

describe 'Blogs API' do

  path '/blogs' do

    post 'Creates a blog' do
      tags 'Blogs'
      consumes 'application/json'
      parameter name: :blog, in: :body, schema: {
        type: :object,
        properties: {
          title: { type: :string },
          content: { type: :string }
        },
        required: [ 'title', 'content' ]
      }

      response '201', 'blog created' do
        let(:blog) { { title: 'foo', content: 'bar' } }
        run_test!
      end

      response '422', 'invalid request' do
        let(:blog) { { title: 'foo' } }
        run_test!
      end
    end
  end

  path '/blogs/{id}' do

    get 'Retrieves a blog' do
      tags 'Blogs'
      produces 'application/json', 'application/xml'
      parameter name: :id, in: :path, type: :string

      response '200', 'blog found' do
        schema type: :object,
          properties: {
            id: { type: :integer },
            title: { type: :string },
            content: { type: :string }
          },
          required: [ 'id', 'title', 'content' ]

        let(:id) { Blog.create(title: 'foo', content: 'bar').id }
        run_test!
      end

      response '404', 'blog not found' do
        let(:id) { 'invalid' }
        run_test!
      end

      response '406', 'unsupported accept header' do
        let(:'Accept') { 'application/foo' }
        run_test!
      end
    end
  end
end

「RSpecで振る舞いを記述してそこからJSONを生成するというのは、TDD(Test Driven Development)的な、いやむしろBDD(Behavior Driven Development)的なアプローチを感じますね」「なるほど!」「書いたRSpecはそのままテストコードになるんでしょうね」

参考: テスト駆動開発 - Wikipedia
参考: ビヘイビア駆動開発 - Wikipedia

「実際に使ってみないとわかりませんが、小規模なSwagger+Rails案件をRailsエンジニアだけで開発するなら、このrswagを使ってみてもいいかもと思いました👍」「お〜」「この種の特殊なライブラリは、大規模な案件や複雑な案件でいきなり使うと途中で機能が足りなくなる可能性も考えられるので、最初は小規模な案件で試すのがいいでしょうね」

参考: とにかくRails6でrswagを動かす - Qiita

🔗 AS句その後


つっつきボイス:「そうそう、この間のAS句(ウォッチ20210301)について@kamipoさんがツイートしてましたね」

「興味深いのは、DBの型情報を付けているのがDBのアダプタだということ: 仮にもっと賢いアダプタを作ってそこで型情報を付与できれば、テーブルがなくてもAS句で作ったクエリでActive Recordの型付きオブジェクトを取ることが原理的には可能ということになりますね」「なるほど」「ただ、型情報の付与を厳密にやろうとすると複雑さが増して処理が重くなる可能性はありそう」

🔗 Active Recordクエリをビューに書いてもいいと思う場合(Hacklinesより)


つっつきボイス:「冒頭で『Active Recordクエリをビューに書いてはいけない』とよく言われていることを踏まえて、でもたとえばCategory.allぐらいだったらビューに直接書いてもいいのではという記事でした」「プロジェクトの方針にもよりますが、セレクトボックスの項目をCategory.allで取るぐらいならビューに書いても構わないだろうという気持ちはわかりますね: その一方で『Active Recordクエリをビューに書いてはいけない』と言われる理由もわかります」「なるほど」

「どこまで潔癖にやるかは最終的にプロジェクトの方針次第でしょうね」「開発速度重視のプロトタイプ開発なら、短期間で開発できるRailsのメリットを活かすためにもビューに書くのをありにしてもいいかもと思いました」


同記事の末尾でも「ビューにロジックを置かないようにすれば確かにコードも変更しやすくなるし理解しやすくなる」「でも他の部分にまず影響しない場合なら置いてももいいのでは」と締めくくられています。

🔗 その他Rails


前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210303後編)Bundlerのセキュリティ修正、Rubyのガベージコレクション記事、Rubyが2/24に誕生日ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines

週刊Railsウォッチ(20210309後編)RubyのIRBに隠れているイースターエッグ、Power Automate Desktop、SQLクエリのありがちなミス6つほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 RubyのIRBに隠れているgem(Ruby Weeklyより)


つっつきボイス:「gemでないものも含めてIRBにはいろんなものが入っているという記事です」

「Ruby 2.7から入ったと記事に書かれているreline gem↓もIRBで使われている」「relineといえば、@aycabtaさんが心血を注いだgemですね」「他にRipper(パーサー)も使われている」

ruby/reline - GitHub

参考: class Ripper (Ruby 3.0.0 リファレンスマニュアル)

RubyKaigi Takeout 2020の動画を見つけました。

loaderというextensionが入っている: こんなふうにIRBのコンテキストでファイルを読み込んで評価できる↓」「おぉ〜」「-r付けてファイルを読み込む、そういえばやったことあったかも」「irb_loadでIRBの中でもファイルを読み込めるのか」

# 同記事より
$ irb -r ./hi.rb
Hi codenamev!
irb(main):001:0>

参考: ruby/loader.rb at master · ruby/ruby
参考: ruby/loader.rb at master · ruby/ruby · GitHub

use_tracerは自分も使ってます」「最近入ったmeasureはウォッチでも話題にしましたね(ウォッチ2021_02_02)」

参考: ruby/tracer.rb at master · ruby/ruby
参考: ruby/measure.rb at master · ruby/ruby · GitHub

ruby/tracer - GitHub

「以前だとpryを入れないとできないことがありましたけど、今はRuby標準のIRBでできることがとても増えましたよね」「IRBはすごく使いやすくなったと思います」

pry/pry - GitHub

🔗 IRBに隠された「イースターエッグ」

「お、IRBにはイースターエッグが隠されているようですよ」「どれどれ👀」「こんなのが隠れてたなんて!」


同記事より

参考: ruby/easter-egg.rb at master · ruby/ruby

「Ruby 3.0のIRBを立ち上げてIRB.send :easter_eggと入力したら動きました!」「このeaster-egg.rbを明示的に読み込めば他のバージョンのRubyでも動かせそうですね」

「手元のRuby 2.7.1だと以下のような静止画↓で表示されたけど、easter-egg.rbのソースを見るとtypeに:logo:dancingがある: ということは、IRB.send :easter_egg, :dancingと入力すると…動くイースターエッグが出た!🎉

(注: 微妙に色が付いているのはhachi8833のiTerm設定によるものです)


ちょうどもうすぐイースターなので季節感ありますね。

後で手元のrbenv環境でRubyのバージョンを変えて試したところ、easter-egg.rbはどうやらRuby 2.7.1から入ったようです。Ruby 3.0.0から動くイースターエッグがデフォルトになったんですね。もしやと思って履歴を調べると、動くバージョンを入れたのはやはりmameさんでした。

ついでにeaster-egg.rbをローカルにダウンロードし、easter-egg.rbが入っていないRuby 2.7.0でirb -r ./easter-egg.rbを実行してIRB.send :easter_eggを実行すると、動くイースターエッグが表示されました。

🔗 bundle openでgemを検索・デバッグする(Ruby Weeklyより)


つっつきボイス:「Bundlerにもいろいろ機能が隠れていますね」「bundle open gem名でgemをエディタで開けるらしい」「このopenってもしかしてMacのopenコマンドにしか対応してないのかなと思ったら、Ubuntuでも動いたので大丈夫ですね」「お、なるほど」

「自分も手元でGemfileのあるプロジェクトでやってみたら環境変数でエディタを設定しろと言われたので↓、BUNDLER_EDITOR=vi bundle open nokogiriとしたら動きました」

$ bundle open nokogiri
To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR

🔗 Enumerable#filter_mapRuby Weeklyより)

↑記事の末尾にあります。filter_mapはRuby 2.7で入ったそうです。


つっつきボイス:「小ネタですが、Ruby Weeklyの末尾に載ってるTip of the Weekを拾いました」「filter_mapという名前からして、filtermapを一度にできそう」

参考: Enumerable#filter_map (Ruby 3.0.0 リファレンスマニュアル)

「まさにfilterしたものに対してmapする処理を1個のブロックでまとめて書けるんですね↓」「これは使いたい機能👍

# Ruby Weekly #541より
[1,2,3,4,5,6].filter_map { |x| x ** 2 if x.even? }
#=> [4,16,36]

「ところでfilter_mapはあるけどselect_mapはないらしい」「言われてみればfilterselectってどちらかがエイリアスでしたっけ?」「ドキュメントを見ると、Rubyのfind_allselectfilterは等価ですね↓」

参考: Enumerable#filter (Ruby 3.0.0 リファレンスマニュアル)

🔗 RubyのnotHacklinesより)


つっつきボイス:「シンタックスハイライトなしでベタに書くと混乱するタイトルをわざと使ってますね☺

後でタイトルを手元でハイライトしてみました↓。最後の「(not)」は、「でも実は同じではない」というニュアンスのようです。

Ruby’s not keyword is not not but !(not)

「Rubyの否定演算子といえば!ですけど、notってあったかな?」「自分も知らなかったんですが、探すとnotキーワードがありました↓」「not!よりも優先度が低い、なるほど」

参考: 演算子式 (Ruby 3.0.0 リファレンスマニュアル)

# docs.ruby-lang.orgより
高い   ::
       []
       +(単項)  !  ~
       **
       -(単項)
       *  /  %
       +  -
       << >>
       &
       |  ^
       > >=  < <=
       <=> ==  === !=  =~  !~
       &&
       ||
       ..  ...
       ?:(条件演算子)
       =(+=, -= ... )
       not
低い   and or

「Rubyのnotは、andorの次に優先順位が低いんですね」「スペルアウトしたandorがあるならnotがあるのもわかる気がする」「記事では!notを再定義してみてますね↓」「notを実際に使うことはあまりなかったな」

# 同記事より
# ~/code/ruby/rubys_not_is_not_not_but_!_(not).rb

class Banger
  def !
    :bang
  end
end

class Naysayer
  def not
    :nay
  end
end

(Ruby 1.9の)ドキュメントにあるように、Rubyの!はオーバーライドできるので、! message()のようにメッセージの前に置いても値をオーバーライドできます。しかしnotキーワードのオーバーライドは、instance.not()のようにインスタンスの戻り値にしか効きません。
!をオーバーライドするとnotにも効くので、notは何らかの形で!を使っていることがわかります。
ただし!notは機能(つまり優先順位)が異なるので、互いにエイリアスではありません。
同記事より大意

🔗 その他Ruby


つっつきボイス:「よく言われている話ですが一応取り上げてみました」「『macOSに最初から入っているRubyは使わないこと』というのはMacで開発するときの常識になっていますが、初心者がハマる原因でもありますね」

「最近Mac使っていないので昔の話になりますが、macOSに最初から入っているRubyはバージョンが古かったし、記事にもあるようにgemをインストールするのにroot権限を要求されたりとかいろいろ不便でしたね」「あれはつらかったです…😢

「今のmacOSに入っているRubyのバージョンっていくつなんだろう?」「あ、そういえば今はどうなってるのかな?」


後で調べると、自分のMacBook(OSはCatalina: 10.15.7)の/usr/bin/rubyは2.6.3p62でした。

$ /usr/bin/ruby --version
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]

今ならウォッチでも取り上げたRails Girlsのインストールレシピ↓を参考にするのがよいでしょうね(ウォッチ20210126)。

参考: Rails Girls インストール・レシピ: Rails Girls - Japanese

🔗DB

🔗 SQLクエリのありがちなミス6つ(DB Weeklyより)

つっつきは6つから抜粋しました。

🔗 NULL値に対してNOT INを使うミス

つっつきボイス:「記事にあるNOT IN (c001, c003, c004, NULL)のように書いたり、なんとか=NULLのように書いたりしても期待どおりには評価されないので、基本的にはカラム定義でNULLを許すときは気をつける必要がありますね」「たしかに」「なお、SQL標準でも多くのRDBでもNULL = NULLはfalseになります」

「RailsのActive Recordではそういう場合にIS NOT NULLをAND条件でつないでくれますが、生SQLを書くときには自分でこうした点に注意する必要があります」

🔗 クエリの句の順序が正しくないミス

SELECTFROMWHEREの次は、『GROUP BYしてHAVINGしてからORDER BYする』という順序で書くのがSQLの構文レベルで決まっているので、順序を間違えるとその時点でシンタックスエラーになります」

-- 同記事より: 間違っている例
SELECT empName 
FROM employees 
WHERE empCategory='DevOps' 
ORDER BY empName 
GROUP BY branchCode 
HAVING count(*) = 1;
-- 同記事より: 正しい例
SELECT empName 
FROM employees 
WHERE empCategory='DevOps' 
GROUP BY branchCode 
HAVING count(*) = 1
ORDER BY empName;

🔗 BETWEENを両端の値を含まない範囲指定として使うミス

「そうそう、SQLのBETWEENは両端の値を含むのが要注意ですね: 4月中の日付で取ろうとして以下のように書いてしまうと、5月1日のものまで取ってしまう↓」

-- 同記事より
SELECT *
FROM products
WHERE manuf_date BETWEEN ‘2020-04-01’  AND ‘2020-05-01’

「そういえば学校で教える英語では常識的にbetweenは両端を含まない(exclusive)なので、英語圏の人がそこで間違えやすそうですね」「そうそう、自分もそこが昔から気になっているんですが、どういうわけかSQLのBETWEENは境界値を含む(inclusive)仕様になっているんですよ」「英語圏では直感に逆らいそう…」

参考: [SQL] 5. データの参照 5 | TECHSCORE(テックスコア)

「少なくとも翻訳の世界では、たとえば”between 2020-01-01 and 2020-02-01″と書かれていたら、1月1日と2月1日は含まないと解釈するんですが、原文が正しくそのつもりで書かれているとも限らないのが要注意だったりします」「へ〜」「ちなみに境界値を含めたい場合はfromとtoで書きます」「たしかにfromとtoで書く方が誤解されなさそう」

「SQLのBETWEENはそうやって面倒になりがちなので、最終的には以下のように書くことが多いかな↓」「なるほど」「これもRailsのActive Recordなら、rangeで指定するとinclusive/exclusiveに応じてBETWEENを使うか使わないかを自動で判断してくれますね」

-- 同記事より
SELECT *
FROM products
WHERE manuf_date => ‘2020-04-01’
AND manuf_date < ‘2020-05-01’

後で調べて思い出しましたが、英語のbetweenは「その期間のどこかの時点」という単発的なニュアンスを含み、fromとtoだと「その期間ずっと」という継続のニュアンスを含むことがよくあります。

参考: 7つの不思議な仕事?(from A to Bの違い?) | 実践で使える英語をマスター!GLJ英語学院/ビズ英アップ!スクール

ついでながら、英語の”between you and me”は「ここだけの話なんだけど」「これは二人だけの秘密だよ」というイディオムです。

🔗 実行時の暗黙のフィールド定義型変換

「これはRDBMSに依存する部分も大きいのでなかなか厄介な問題ですね: 以下のpinがvarcharの場合暗黙で強制変換されて、ランタイムエラーにこそならないけどパフォーマンスが落ちることがある」

-- 同記事より
SELECT *
FROM myAccount acc
WHERE acc.pin = 123654789286
AND acc.isPending IS null;

「MySQLはこういう場合に割といい感じに型変換してくれますが、PostgreSQLはデフォルトだとお固い傾向がありますね」「なるほど」「PostgreSQLもimplicit conversionを指定すればMySQL並に型変換できるようになりますが、それらも含めてこの問題はRDBMSによって変わってきます」

参考: PostgreSQL 12.4文書 CREATE CAST

「これについてもActive Recordはよしなに対応してくれるので、自分でクエリを書くときもActive Recordが出すクエリを参考にすれば、MySQLでは通るのにPostgreSQLでは通らないということを避けたいときにいいでしょうね」「なるほど!」

🔗 kamipo TRADITIONAL

「特にMySQLを使う場合は、通称『kamipo TRADITIONAL』を参考にするといいと思います」「お、何ですかそれ?」「これです↓」

参考: ルーク!MySQLではkamipo TRADITIONALを使え! | おそらくはそれさえも平凡な日々

「記事にもあるようにsql_modeを以下のような感じで設定しておくと、MySQLでも標準SQLに近いクエリしか通らなくなりますので、そうした問題を回避できます」「知らなかった〜」

# https://songmu.jp/riji/entry/2015-07-08-kamipo-traditional.html より
SET SESSION sql_mode='TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'

「特にONLY_FULL_GROUP_BYは付けることを習慣づけておくといいと思います: ただMySQLの楽な書き方に慣れていると、SELECTするカラムが多い場合にGROUP BYをすべて書くのが面倒に感じられたりもするんですよね…」


「以上、昔からよくあるありがちなSQLのミスでした」

🔗言語/ツール/OS/CPU

🔗 マイクロソフトのRPA「Power Automate Desktop」(Publickeyより)


つっつきボイス:「ちょっと試してみたそうですね」「そうそう、こんな感じでEC2インスタンスを取得したりできます↓」

「Power Automate Desktopは、一度動かすと右のペインで変数の値を参照できたりするのがなかなかよくできていると思いました」「お〜、ExcelのVBA画面みたいですね」「少しずつ動かしながら変数の値を確認して書き足す、といった使い方ができます」

参考: Visual Basic for Applications - Wikipedia

「もちろんまだ成熟していない機能もいろいろありますが、変数の宣言方法以外はドキュメントをほとんど見ないで書けたのはよかった」「へ〜!」「Power Automate Desktopは前から有料版があったのが無料でもダウンロードして利用できるようになったそうです」「記事にも今後はWindows 10に標準搭載されるとありますね」

参考: Power Automate Desktop のよくある質問集 - 吉田の備忘録

「Macに入っているApple ScriptのAutomatorよりはイケてる感じでしょうか?」「Automatorはほとんど使ったことがありませんが、感覚的にはZapierに近いかな」「なるほど」

参考: Automator Actionを実行 – AppleScriptの穴
参考: Zapier | The easiest way to automate your work

「Power Automate Desktopはプログラマーでない人でも作ることができて、この種のツールとしては比較的機能が揃っているのがいいですね: プログラマー向けにはPythonスクリプトの実行やSFTP接続といった項目もあります」「ホントだ」「ちょっとした日々の運用タスクをある程度自動化するのによさそう👍

🔗 cosmopolitan: 複数環境で実行できるポータブルバイナリを出力(StatusCode Weeklyより)

jart/cosmopolitan - GitHub

cosmopolitan: 国籍や国境にとらわれない国際人


つっつきボイス:「マルチな環境で動くシングルバイナリを出力できるそうです」「リポジトリに貼ってある動物の絵はスカンクかな?」「Linux + Mac + Windows + FreeBSD + OpenBSD + NetBSD + BIOSで動くって、そんなことが可能なのかしら?」

「リポジトリに謎のタイトルの関連ブログ記事が貼ってある↓」「あ、それはニセのギリシャ語で書かれた英語タイトルです」「ニセギリシャ語😆」「記事を眺めた限りでは、シングルバイナリをマルチな環境で動かす方法があるらしい」「へ〜!」

参考: αcτµαlly pδrταblε εxεcµταblε

「以下のredbeanも同じ人がcosmopolitanをベースに作ったそうです」「実用性はともかく技術的なチャレンジとして面白い」「誰得感ありますね」「スゴい人がいるもんだ…」


後編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210308前編)書籍『Ruby on Rails Performance Apocrypha』、rswag gemほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

Hacklines

Hacklines

StatusCode Weekly

statuscode_weekly_banner

Publickey

publickey_banner_captured

DB Weekly

db_weekly_banner

実践Capistrano 3(1): タスク、ロール、変数(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

実践Capistrano 3(1): タスク、ロール、変数(翻訳)

Capistranoは、Webアプリケーションのデプロイやメンテナンスを自動化するのに有用なツールです。私の場合、Ruby on Railsアプリケーションやその他のRackベースのアプリケーションのデプロイに用いています。依存関係のインストール、アセットのコンパイル、データベーススキーマのマイグレーションといった厄介な作業は、Capistranoがまとめて面倒を見てくれます。Capistranoを使わない場合、手動でサーバーにsshログインして必要なコマンドを手入力することになりますが、長時間に渡る忍耐力が求められますし、うっかりミスにつながりかねません。ありがたいことに、今やそうした作業は必須ではありません。

Capistranoの設計はモジュラーなので、プラグインやスクリプトを多数利用できます。これらを用いることで、要求の厳しいデプロイも可能になります。欲しい機能が見当たらなくても、Capistranoは本質的に柔軟なので、タスクを簡単に書き下ろせます。どのようなデプロイにしたいかをCapistranoに正確に指示すると、Capistoranoはサーバーを準備してアプリケーションを最新に保つよう支援します。

本シリーズにおける私の目標は、Capistranoというデプロイツールの中心となるコンセプトを解説するとともに、多くの高度な機能をご紹介することです。そのために、Rubyで書かれたWebアプリケーションをメンテナンスする場合に、現実的によくある問題のソリューションをさまざまな事例で見ていきます。これらの事例によって、作業を短時間で完了できるCapistranoスクリプトの書き方をもれなく学ぶことができ、Capistranoの内部に関する勘も養えるようになるはずです。

本記事のコードサンプルは、すべてCapistrano 3を前提としています。執筆時点では、Capistrano 3.11.2をインストールすることになります。Capistranoを自分のプロジェクトでセットアップする方法について詳しくは、公式ガイドをご覧ください。

前置きはこのぐらいにして、早速始めましょう!

Capistranoのタスク

Capistranoのコア部分では、Rakeビルドツールが提供するタスクを用いて、アプリケーションのアップデートやデプロイの操作を記述します。Capistranoはそれらのタスクを直感的な独自DSLにまとめ、ローカルコンピュータやリモートサーバー上のUnixシェルで実行しやすい形にします。

私は長い間、あっちこっちのサーバーに手動でsshログインしてはその場限りのアップデートに明け暮れていたこともありました。手動作業では、たとえばstagingサーバーにアクセスしたい場合に、デプロイユーザーだのサーバー名だのをいちいち把握した上でシェルに入力する必要がありました。これでは、つらいうえに操作ミスも起こりがちです。

「なるほど、ではもっといい方法はないものか?」

詳細情報を一切把握せずに、デプロイ環境であらゆるサーバーに同じ方法でログインできれば理想的です。つまり「stagingサーバーにログインしたい」と一言で済ませたいということです。Capistraoでは、以下のコマンドを実行することがこれに相当します。

$ cap staging login

このコマンドを「タスク」として実装する方法を見ていきましょう。

Capistranoは、デフォルトでlib/capistrano/tasksディレクトリのファイルを読み込みます。このディレクトリの下にlogin.rakeというファイルを作成します。*.rakeという拡張子は、Railsタスクを含むRubyファイルでよく使われます。Rakeはdescというメソッドを提供しますが、これはタスクにdescription(詳細)を追加できます。記述したdescriptionは、すべての有効なタスクを表示するcap --tasksを実行すると表示されます。Rakeにはtaskというメソッドもあり、これはタスクの振る舞いを説明するのに用いられます。以下のようにdescriptionの直後にtaskブロックを置きます。この新しい:loginタスクは取りあえず空にしておきます。

# lib/capistrano/tasks/login.rake

desc "Login into a server based on the deployment stage"
task :login do
  ...
end

このままではloginタスクで何も行われません。ログインするには、アプリケーション・サーバーのログイン情報を集める必要がありますが、ここで「ロール(role)」という概念についてお話ししておく必要があります。

Capistranoのロール

Capistranoでは、どのタスクをどのサーバーで実行すべきかをきめ細かに制御するためにロールという概念を用います。たとえば、データベースサーバーにのみ適用したいが、Webサーバーにデプロイするときはスキップしたいタスクがあるとします。ロールは、ちょうどフィルタのように動作するので、特定のロールにマッチするサーバーでタスクを実行する前に、同じロールでひとまとめにグループ化するようCapistranoに指示できます。

Capistranoタスクでは、以下の3つのロールがよく使われます。

appロール
アプリケーションサーバー(コンテンツを動的に生成するサーバー)で実行するタスクに用いる。これは、Railsではpumaサーバーに相当する。
Capistranoの組み込みタスクのうち、deploy:checkdeploy:publishingdeploy:finishedはいずれもappロールで実行される。
dbロール
データベースサーバーで実行するタスクに用いる。
例: capistrano-railsプラグインが提供するdeploy:migrate(Railsデータベーススキーマのマイグレーションに用いられる)。
webロール
静的コンテンツを提供するWebサーバーを扱うタスクに用いる。
Nginxの場合、コミュニティの作ったcapistrano3-nginxプラグインではnginx:startnginx:reloadnginx:site:disableといったタスクはすべてwebロールを用いる。

もちろん独自のロールも定義できます。例: Redisデータベースインスタンスに関連するタスクのみを実行するのに用いるredisロール。

role :redis, "redis-server"

サーバーに指定されているロールの種類にかかわらず、タスクをあらゆるサーバーにマッチさせたい場合はallロールを用います。Capistranoの組み込みタスクでは、柔軟性を保つためにallロールが用いられています。

ここでは「アプリケーション」「Webサーバー」「データベースサーバー」をホストする1台のコンピュータにデプロイすることにし、サーバーにappロールとdbロールとwebロールをすべて適用するために、serverメソッドによるショートハンド定義を用います。

# config/deploy/staging.rb
server "staging-server.example.com", roles: %w[app db web], primary: true

上の定義をよく見ると、primary:というプロパティがあることに気が付くでしょう。このプロパティは、タスク実行時の優先順位をCapistranoに通知します。primary: trueを指定したサーバーに関連付けられているロールを持つタスクは、最初に実行されます。これは、アプリケーションを多数のホストに分割している場合に特に便利です。そのような場合は、以下のようにロール中心の定義に組み換えられます。

# config/deploy/staging.rb

role :app, "app-server.example.com"
role :db, "db-server.example.com", primary: true
role :web, "static-server.exmaple.com"

ここで定義したロールをタスク定義に適用する方法については後述します。

ホストの設定ファイルを取得する

サーバー設定ファイルにアクセスするには、rolesヘルパーメソッドを用います。rolesメソッドはロール定義を1つ以上受け取り、マッチするすべてのホストのインスタンスをリストにして返します。ここでは:appロールを指定すると、Capistrano::Configuration::Serverという1台のサーバー設定インスタンスだけがリストに含まれます。

Capistrano 3ではさまざまなイノベーションが行われていますが、よりモジュール性の高いアーキテクチャの導入もそのひとつです。Capistrano 3では、sshセッション管理もRubyのsshkit gemという別の依存関係に移行しました。SSHKitが提供するDSLを用いるとonというメソッドが導入され、ブロックスコープで記述したコマンドをさまざまなサーバーで実行できます。onメソッドはホスト設定オブジェクトの配列を受け取ると、SSHKit::Coordinatorを用いてコマンドをホストごとにパラレル実行します。

# lib/sshkit/dsl.rb

module SSHKit
  module DSL
    def on(hosts, options={}, &block)
      Coordinator.new(hosts).each(options, &block)
    end
  end
  ...
end

設定済みホストが複数ある場合は、おそらく次のようにin: :sequenceを用いて各サーバーに順番にログインするようCapistranoに指示する方がよいでしょう。

on roles(:app), in: :sequence do |server|
  ...
end

onのスコープ内では、サーバーに関する情報を含むホストインスタンスにアクセスできるようになります。本質的に「このサーバーで以下の作業を行え」と指示するのと同じです。

# lib/capistrano/tasks/login.rake

desc "デプロイのステージに応じてサーバーにログインする"
task :login do
  on roles(:app) do |server|
    ...
  end
end

Capistranoの変数

サーバーにssh接続する場合、「ユーザー名」「サーバー名」「ログイン先のパス」を知る必要があります。Capistranoではsetメソッドが提供されています。グローバルなタスクや特定のタスクを設定する変数をこれで設定すると、スクリプトの他の部分でも利用できるようになります。たとえば、以下のように:user変数や:deploy_to変数を設定できます。

# config/deploy/staging.rb

set :user, "deploy-user"
set :deploy_to, "/path/to/deploy/directory"

Capistranoが提供するfetchメソッドを使えば、設定変数を楽に読み出せます。たとえば、以下のようにユーザー名やデプロイパスを取得できます。

# lib/capistrano/tasks/login.rake

desc "デプロイのステージに応じてサーバーにログインする"
task :login do
  on roles(:app) do |server|
    user = fetch(:user)
    path = fetch(:deploy_to)
    ...
  end
end

これでユーザー名とログイン先パスを取得できたので、sshコマンドユーティリティに引数を渡してURIを組み立てられるようになります。URI文字列は「ユーザー名」「サーバー名」「ポート番号」を結合してビルドします。以下のコードでは、ユーザー名やポート番号が指定されていない場合を扱います。

# lib/capistrano/tasks/login.rake

desc "デプロイのステージに応じてサーバーにログインする"
task :login do
  on roles(:app) do |server|
    user = fetch(:user)
    path = fetch(:deploy_to)

    uri = [
      user,
      user && '@',
      server.hostname,
      server.port && ":",
      server.port
    ].compact.join
  end
end

sshコマンド

これで、sshコマンドにURIとデプロイパスを渡して実行できるようになりました。先に進む前にssh周りについて軽く補足しておきます。

特に-tフラグはsshユーティリティに「teletypeモード」で実行するよう指示します。teletypeモードとは何でしょう?Capistranoは、デフォルトではssh接続でのデプロイ時にビジュアルターミナルをアタッチしません。誰も見ようがないので、洗練されたインターフェイスを表示する必要はないということになります。しかし本記事では、ユーザーがシェル機能をすべて利用できるようにしておきたいと思います。

-tフラグに続けてサーバーのURIを指定し、続いて、ログイン後に1回実行するシェルコマンドを引用符で囲みます。ここでは2つのシェルコマンド文字列をつなげて使います。1つ目はWebサイトのルートディレクトリに移動するコマンド、2つ目は$SHELL変数の実行です。この$SHELLは、デフォルトシェルの位置を表す環境変数で、通常はBashになっています。シェルをわざわざこの形で起動する必要がある理由は何でしょうか?sshは、シェルを実行しないと接続を終了してログアウトしてしまいます。このタスクでやりたいのはログインを継続することなのです!

bashを指定してシェルを実行する場合、-lフラグを指定すると、ユーザーがログインしているかのようにシェルを呼び出します(対話モード)。ここでは、.bash_profileなどの隠しファイルにある設定をすべて事前に読み込んでおくために使っています。

最終的なコマンド文字列は以下のようになります。

"ssh -t #{uri} 'cd #{path}/current && exec $SHELL -l'"

Rubyのexecを用いて、sshコマンドをローカルで実行します。

We use Ruby’s exec to run our ssh command locally:

exec("ssh -t #{uri} 'cd #{path}/current && exec $SHELL -l'")

以上をまとめると、以下のようなタスクのできあがりです。

desc "Login into a server based on the deployment stage"
task :login do
  on roles(:app) do |server|
    user = fetch(:user)
    path = fetch(:deploy_to)

    uri = [
      user,
      user && '@',
      server.hostname,
      server.port && ":",
      server.port
    ].compact.join

    exec("ssh -t #{uri} 'cd #{path}/current && exec $SHELL -l'")
  end
end

ここまでは、自分のコンピューターからサーバーに手早くログインする方法について説明しました。しかしCapistranoで日々の作業を強化する方法は、これだけではありません。

リモートのRailsコンソール

私の場合、サーバーのRailsコンソールを取りあえず開いて、今動いているデータベースにクエリをかけなければならなくなることがよくあります。CapistranoのログインタスクでログインしてからRailsコンソールを開くコマンドを手打ちしてもいいのですが、いっそproductionのRailsコンソールを一発で開くCapistranoスクリプトを使ったらどうでしょう?以下のようなコマンドを実行してやれるようにするというイメージです。

$ cap production console

上のコマンドを実装するには、以下の「ベアボーン」:consoleタスクとdescriptionを、同じlogin.rakeファイルの中に書きます。

# lib/capistrano/tasks/login.rake

desc "デプロイのステージに応じてRailsコンソールを開く"
task :console do
    ...
end

ログインタスクのときと同様、「ユーザー名」「デプロイのパス」を知る必要があります。Railsコンソールを開く場合は、現在Railsを実行している環境の名前も必要になります。これはcapistrano-rails gemで設定される:rails_env設定変数から取れます。

# lib/capistrano/tasks/login.rake

desc "デプロイのステージに応じてRailsコンソールを開く"
task :console do
  on roles(:app) do |server|
    env = fetch(:rails_env)
    user = fetch(:user)
    path = fetch(:deploy_to)
    ...
  end
end

Railsの実行可能ファイルを探索する

sshでRailsコンソールを開く前に、もうひとつ対処が必要になります。そのままではsshの仮想ターミナルにアクセスできないので、ユーザープロファイルの設定ファイルを読み込めません。そこで、どうにかしてCapistranoがrails実行可能ファイルを探索し、そしてインストールされているRubyを探索できるようにする必要があります。私はRubyのインストールをrbenvユーティリティで管理し、gemのインストールをBundlerで管理するのが好みです。他の方法でRubyを管理している方は、適宜パスを調整してください。

rbenvのパスについては、デプロイするユーザーの$HOME/.rbenvで指定されるローカルのインストール場所を用いることにします。rbenvの「shims」という概念は、実際にインストールされているgemirbrailsなどのRubyコマンドをrbenv execコマンドに対応付けるのに使われます。railsコマンドを実行する場合、shimをbundleして、必要な依存関係をすべて読み込むようにする必要があります。利用できるbundle実行ファイルは$HOME/.rbenv/shimsの中にあります。

最後の手順として、-eフラグでrails consoleコマンドに現在の環境の情報を伝えます。Railsコンソールを開く完全なコマンドは以下のようになります。

console_cmd = "$HOME/.rbenv/shims/bundle exec rails console -e #{env}"

パズルの最後のピースがはまったので、以下でRailsアプリケーションにssh接続してRailsコンソールを開けるようになります。

"ssh -t #{uri} 'cd #{path}/current && #{console_cmd}'"

最後は、いよいよひとつのスクリプトにまとめてみましょう。

desc "デプロイのステージに応じてRailsコンソールを開く"
task :console do
  on roles(:app) do |server|
    env  = fetch(:rails_env)
    user = fetch(:user)
    path = fetch(:deploy_to)

    uri = [
      user,
      user && '@',
      server.hostname,
      server.port && ":",
      server.port
    ].compact.join

    console_cmd = "$HOME/.rbenv/shims/bundle exec rails console -e #{env}"

    exec("ssh -t #{uri} 'cd #{path}/current && #{console_cmd}'")
  end
end

重複の除去

本記事を鵜の目鷹の目で読んでいる方であれば、タスクとタスクの間に少々冗長な繰り返しがあることにお気づきかと思います。重複を退治してタスクを強化し、メンテナンス性を高ることにしましょう。私は、コードの繰り返し部分をいったん「完全な繰り返し」に揃えてから重複を取り除くのが好みです。こうすることで、コード内のまったく同じ部分がきれいに浮かび上がってくるからです。今回の場合、Railsの環境変数や実行するsshコマンドの部分を別にすれば、タスクは完全に同一です。

重複削除の精神に則って、セットアップ部分やsshコマンド実行部分をrun_ssh_withという独自のメソッドに移動することにします。このメソッドは、サーバー設定と実行すべきコマンドを引数に取ります。

# lib/capistrano/tasks/login.rake

def run_ssh_with(server, cmd)
  user = fetch(:user)
  path = fetch(:deploy_to)

  uri = [
    user,
    user && "@",
    server.hostname,
    server.port && ":",
    server.port
  ].compact.join

  exec("ssh -t #{uri} 'cd #{path}/current && #{cmd}'")
end

run_ssh_withにまとめたおかげで、どちらのタスクもシンプルになりました。

# lib/capistrano/tasks/login.rake

desc "デプロイのステージに応じてサーバーにログインする"
task :login do
  on roles(:app) do |server|
    run_ssh_with(server, "exec $SHELL -l")
  end
end

desc "デプロイのステージに応じてRailsコンソールを開く"
task :console do
  on roles(:app) do |server|
    env  = fetch(:rails_env)
    console_cmd = "$HOME/.rbenv/shims/bundle exec rails console -e #{env}"
    run_ssh_with(server, console_cmd)
  end
end

随分見違えましたね!しめくくりに、あらゆるキー入力を短く済ませたがるプログラマーの本性をなだめることにしましょう。2つのタスクにはエイリアスを作成できるのです!rakeそのものにエイリアス機能はありませんが、それらしくすることは可能です。その方法とは、実行内容が最初に別のタスクに依存する新しいタスクを定義することです。2つのタスクを1文字に短縮してみましょう。

# lib/capistrano/tasks/login.rake

task :c => :console
task :l => :login

まとめ

怒涛のようなCapistranoツアーが終わりました。Capistranoスクリプトを書いたことのない人は本記事で多くのことを学べます。願わくば、Capistranoスクリプトを書いたことがある方にとっても機能のいくつかが明確になることでしょう。本記事では、Capistranoタスクのしくみ、変数の設定方法、コマンドをローカルで実行する方法について一般的なことを学べます。

ログインタスクとコンソールタスクを使えば、繰り返しに満ちた作業を手軽に自動化できます。タスクにすることで「Railsプロジェクト間の作業を統一できる」という重要なオマケもあります。リモートサーバーにクエリをかけるのにプロジェクトファイルを急いで開いて接続情報を知る必要がなくなります。ひとつひとつのタスクによる効率化はささやかですが、時間をかけてタスクを蓄積していけばスムーズな開発フローを得られます。同じ用に便利なCapistranoタスクをご存知の方がいらっしゃいましたら、ぜひ共有してください!

私はPiotr Murachと申します。これまで蓄積したプログラミング経験をドキュメント化し、日々の作業を改善できる実用的なコード例とともに皆さんにご紹介しています。私のブログ記事やオープンソースプロジェクトをお楽しみいただけましたら、GitHub Sponsorsで応援お願いします。ニュースレターRSSフィードもぜひどうぞ。

関連記事

productionやdevelopment、stagingという言葉の使い分けについて

Viewing all 1100 articles
Browse latest View live