Rubyで文字列の配列を全て整数に直したいとき
num_arr = str_arr.map &:to_i
みたいな謎のサンプルコードを見たことはありませんか?
またアルゴリズム問題等で標準入力を取得し整数の配列にしたいときのサンプルコードとして
numbers = gets.split.map &:to_i
みたいな意味わからないコードを見たことはありませんか?
これについて自分が理解した限りのメモを残します。
gets.split
これは余談ですが一応。標準入力から1行とって空白で区切って配列にします。最終的にto_intで整数化するためchompでの改行削除は省略します。
ここまでで標準入力が’1 2 3\n’だとしたら、データは[‘1’, ‘2’, ‘3\n’]という文字列の配列になっています。
map
Enumerable#map(EnumerableはArrayの親クラス)はブロックを受け取り、配列の各要素を引数としてブロックを評価し、結果を配列にして返すメソッドです。これによって各要素にto_iを行い整数の配列とするのが今回のコードの目的です。
わかりやすく書くと
a = gets.split.map {|n| n.to_i}
こうで、これを理解するのは簡単だと思います。ブロックをそのまま記述していますから。これがどうして&:to_iと省略されるのかというのが今回の話です。
Procクラス
ブロックをオブジェクト化したものがProcオブジェクトです。{|n| n.to_i}のようなブロックをオブジェクト化したものだと思ってください。Rubyでは{}やdo endで書いたブロックを=でそのまま変数に代入できないため、Procオブジェクトを変数に入れたい場合
prc = Proc.new {|n| n.to_i}
のようにProcコンストラクタを通します。
再びmapの引数、そして&
ここまででmapに与えたいブロックがProcオブジェクトとして表現できるということがわかりました。では前述の例にあるprc変数をmapに与えてみるとどうなるでしょうか
prc = Proc.new {|n| n.to_i}
numbers = gets.split.map prc
in `map': wrong number of arguments (given 1, expected 0) (ArgumentError)
ブロック引数ではなく普通の引数として渡ってしまってエラーになります。
ここで必要になるのが&です。&はメソッドにこれはブロック引数ですと伝える効果があります。
ブロック付きメソッドに対して Proc オブジェクトを `&’ を指定して渡すと 呼び出しブロックのように動作します。
メソッド呼び出し(super・ブロック付き・yield) (Ruby 2.5.0)
to_proc メソッドを持つオブジェクトならば、`&’ 修飾した引数として渡すことができます。デフォルトで Proc、Method オブジェ クトは共に to_proc メソッドを持ちます。to_proc はメソッド呼び出し時に実行され、Proc オブジェクトを返すことが期待されます。
なので上記を
numbers = gets.split.map &prc
に変えると動くはずです。
&はオペランド(&の右側)のto_procを呼び出すと書かれていますがProc#to_procはselfを返すので&prcが既にProcなのにto_procを呼び出されても問題ありません。
instance method Proc#to_proc (Ruby 2.5.0)
to_proc -> self
self を返します。
Symbol#to_proc
ここまでで&がオペランドのオブジェクトのto_procを呼んでProc化してブロック引数化するということはわかりました。
では続く:to_iはなんなのでしょう。何故ブロックの引数が1.to_iのように変換されるのでしょうか。:to_iのような:で始まるリテラルはSymbolであり、今回のコードを読み解く鍵はSymbol#to_procの挙動にあります。
instance method Symbol#to_proc (Ruby 2.5.0)
生成される Proc オブジェクトを呼びだす(Proc#call)と、 Proc#callの第一引数をレシーバとして、 self という名前のメソッドを 残りの引数を渡して呼びだします。
つまりSymbolである:to_iはto_procによってProc化されると、callの第1引数がレシーバー(.の左側)、to_iがメソッド名(.の右側)になって実行されるようなProcオブジェクトが自動的に作られるのです。
よって:to_iから作成されたProcのcallがmapから呼び出されると、第1引数として与えられる配列の各要素がレシーバーとなり.to_iが呼び出されるのです。
これがmap &:to_iで各要素に対して.to_iが呼ばれる理由です。
余談:&to_i だとどうなるか
&にオペランドをブロックに変える効果があるのなら&to_iではダメなのかと試した結果
numbers = gets.split.map &to_i
undefined local variable or method `to_i' for main:Object (NameError)
オブジェクト指向言語であるRubyではto_iという名前がいきなり参照できないのでこういうわけにはいかないみたいです。
参考文献:
Rubyでメタプログラミング ~暗黙的に呼ばれるto_procメソッド – (゚∀゚)o彡 sasata299’s blog
コメント