今一つピンとこない Ruby の Method オブジェクトについて解説を試みた。
簡潔さのため,あえていくつかの Ruby の基礎的概念について解説を省いたけれど,読んで分からないところがあったら,どんな初歩的なことでも遠慮なくコメントでお尋ねください。
Method オブジェクトってなんやねん?
よく「Ruby はすべてがオブジェクト」というけれど,これは誤解を招く。
「Ruby ではすべてのデータがオブジェクト」と表現すべきだ。
データでないものはオブジェクトでない。
たとえば変数も制御構造もスクリプトもメソッドもブロックも演算子も,データではないからオブジェクトではない。
さて,メソッドはオブジェクトではないのだが,オブジェクト化することはできる。メソッドをオブジェクト化したものは Method クラスのインスタンスであり,「Method オブジェクト」と呼ばれる。
これは,
〈ブロックそのものはオブジェクトではないが,オブジェクト化することはでき,オブジェクト化したものは「Proc オブジェクト」と呼ばれる〉
ことと似ている。
Method オブジェクトと Proc オブジェクトは,その役割や使い方もよく似ている。
どちらも何らかの処理を表しており,call や [ ] を使って呼び出す(その処理を実行させる)ことができる。
また,Method オブジェクトは Method#to_proc を使って Proc オブジェクトに変換することもできる。
作り方
Object クラスには,Method オブジェクトを作るための method というメソッドがある。
オブジェクトに対し,メソッド名を引数として method を呼び出すと,当該メソッドをオブジェクト化した Method オブジェクトを作って返してくれる。
「は??? なんのこっちゃ?」
サンプルコードを見たら一発で分かる:
s = "Ruby"
m = s.method(:upcase)
これで,m には,〈文字列 "Ruby" をレシーバーとする upcase メソッドをオブジェクト化したもの〉が代入される。
Method オブジェクトは〈何らかのクラスやモジュールにおけるメソッド定義単体をオブジェクト化したもの〉ではなく,特定のオブジェクトに結びついているということが本質的に重要だ。
Method オブジェクトのレシーバーは,Method#receiver で得られる:
m = "Ruby".method(:upcase)
p m.receiver # => "Ruby"
メソッド名は Method#name で得られる:
m = "Ruby".method(:upcase)
p m.name # => :upcase
「だから何なんだよ! こんなの意味あんのかよ!」(イライラ)
使い方
Method オブジェクトは Method#call を使って呼び出すことができる。
m = "Ruby".method(:upcase)
p m.call # => "RUBY"
「は? こんなことして何が嬉しいわけ???」
Method オブジェクトはレシーバーが固定されているため,上の例でいうと〈いろいろな文字列を大文字化する〉のには使えない。
このあたりで「もうええわ,ワシには関係あらへん」と読むのをやめてしまいたくなるね。かつての私もそうだった。
えー,気を取り直して,メソッドが引数を持っているケースを見てみよう。
Integer クラスには,文字列化するメソッド to_s があるが,引数を与えることで $n$ 進法の文字列にできる:
x = 2989
p x.to_s # => "2989" (デフォルトは 10 進法)
p x.to_s(2) # => "101110101101"
p x.to_s(16) # => "bad"
これを Method オブジェクトでやってみよう:
m = 2989.method(:to_s)
p m.call # => "2989"
p m.call(2) # => "101110101101"
p m.call(16) # => "bad"
「いや,あのさ,確かに引数を変えていろいろな結果が得られる Method オブジェクトはできたけどさ,やっぱ何が嬉しいのかわかんねーわ」
では,call の代わりに [ ] を使ってみよう:
m = 2989.method(:to_s)
p m[] # => "2989"
p m[2] # => "101110101101"
p m[16] # => "bad"
「いや確かに短くはなったけどさ,短くなっただけじゃん?」
コードが簡潔に書けることはそれだけで価値があったりもするよ。
「いや,そもそも,固定した一つの数をいろんな $n$ 進法で文字列化するっていう場面があんまり思いつかないんだけど?」
何に使うの?
Method オブジェクトが今一つピンとこないのは,用途がよく分からないせいだろう。
Method オブジェクトの解説を見ても,上記と大差ないサンプルコードが載っていたりして,釈然としない。
「例のための例」でしかないからだ。
Proc オブジェクトに比べて出番が少ない理由の一つは,既に見たように〈レシーバーを固定して同じメソッドを何度も呼び出す〉という場面が,〈さまざまなレシーバーに対して同じメソッドを何度も呼び出す〉場面よりも少ないことだろう。
実際,Method オブジェクトを使うコードとして,レシーバーに関知しない1関数的メソッドをオブジェクト化するものをよく見る。関数的メソッドはレシーバーが何でもいいんだから,〈レシーバーが変えられない〉ことは欠点にならない。
現実にはどういう場面で使われるのか?
ブロックを取るメソッドに & で与える
前振り(Proc の場合)
Method オブジェクトの話をいったん脇に置いておいて,姉妹とも言える Proc オブジェクトの,以下のような使い方を見てみよう。
languages = ["Ruby", "Python", "Rust"]
p languages.map(&:downcase) # => ["ruby", "python", "rust"]
この map(&:downcase) は要素を全部小文字にするものだが,見た目の簡潔さとはうらはらに,ちょっと複雑な機構が働いている。
map はブロックを取るが,ブロックを与える代わりに,Proc オブジェクトを & 付きで渡すと,その Proc オブジェクトがブロックとして渡されたことになる。
しかし,map(&:downcase) の場合,& を付けているのは Proc オブジェクトではなくシンボルである。
この場合,Symbol#to_proc が暗黙に呼び出されて Proc オブジェクトができ,それがブロックとしてメソッドに渡される。
この書き方は,裏の仕組みはややこしいけれど,ひとたび理解して慣れてしまえば思考の経済になる。単に短いというよりは簡潔なのだ。
Proc みたくメソッドに渡す
前振りが長くなった。Method オブジェクトに話を戻そう。
まず,16 進法や 2 進法などのプリフィクス(それぞれ 0x, 0b)の付いた文字列の配列が与えられて,それを整数の配列に変換することを考える。
この目的には,String#to_i でなく Kernel.#Integer が向いている。
p Integer("100") # => 100 (プリフィクス無しは 10 進法)
p Integer("0x100") # => 256 (16 進法なので)
p Integer("0b100") # => 4 (2 進法なので)
このコードの Integer はクラスではなくメソッドだ。
「おい,ちょっと待て。メソッド名は小文字始まりじゃねーのかよ!」
文法的にはそんなルールはない。
とはいえ,大文字始まりのメソッド名は特別な場合に限って使うべきだろう。典型的には
- レシーバーに関知しない関数的メソッドで,
-
Kernelに定義されて(つまりどこででも使え), - 引数を元に,同名のクラスのインスタンスを生成する
ようなメソッドだ。
さて,この Integer メソッドを使うとこんなふうに書ける:
integer_strings = ["100", "0x100", "0b100"]
p integer_strings.map{ |str| Integer(str) }
# => [100, 256, 4]
ふう,ようやく Method オブジェクトの出番が来たぞ。
上記のサンプルは Method オブジェクトを使って以下のように書ける:
integer_strings = ["100", "0x100", "0b100"]
p integer_strings.map(&method(:Integer))
# => [100, 256, 4]
さて,これはどのように解釈されるのだろうか。
まず
method(:Integer)
を見よう。この method( ) は,Object#method の呼び出しだ。
引数が :Integer なので Integer メソッドをオブジェクト化したものを返す。
この method( ) にはレシーバーが書かれていない。ということは〈その場における self〉がレシーバーとなる。
つまり,実際に何がレシーバーになるかは書かれた場所によるのである。
でもそれで構わない。Integer メソッドは Kernel に定義されており,どんなオブジェクトに対しても使えるうえ,そもそもレシーバーによらず同じ動作をする。
ここまで分かったら,つぎは
map(&method(:Integer))
を検討しよう。
& のあとに Proc オブジェクトではなく Method オブジェクトが続いている。
こういう場合,シンボルを与えたときと同様,to_proc メソッドが暗黙に呼ばれることになっている。(参照:Method#to_proc)
結果,これは
map{ |x| self.Integer(x) }
みたいなものなわけだ(self. はあえて書いておいたが,もちろん省略できる)。
これで理解できた。
これは良いものなのか?
「でもよ,この書き方って,大して短くねえよな?」
確かに
# (a) ブロック版
integer_strings.map{ |str| Integer(str) }
# (b) Method 版
integer_strings.map(&method(:Integer))
と並べて比べても,字数では 3 字くらいしか短くなっていない。ブロックパラメーターの名前を長くすればもっと差が出るが,逆にブロックパラメーターが x とかだったりすると,かえって長くなる。
では論理的に簡潔かというと,ブロックパラメーターを書かなくてよいという意味では確かに簡素化されて思考の経済になっているが,〈method メソッドの呼び出し〉という別のものが入り込んで,帳消しになっている感が否めない(笑)2。
こういうところもまた,Method オブジェクトの出番が少ない原因の一つだろう。
Rails のビューで
この節で挙げる例は,Ruby on Rails のものなので,Rails やってない人はごめんなさい。
User モデルに name というカラムがあったとする。
ユーザー一覧とか,特定のユーザーの詳細情報を表示する画面には,当然「氏名」といった文字列を表示することになる。
しかし,ビューファイルに直に「氏名」などと書くことは勧められない。
そのままでは多言語化できないし,あとで「姓名」に変えたくなったとき,あちこちのビューを書き換えることになる。
Rails には i18n の仕組みが備わっており,〈User モデルの name カラムの日本語での名前は「氏名」である〉といったことを一箇所で指定しておけば,ビューでその文字列を呼び出すことができる。
呼び出し方は複数あるが,一つの方法は
User.human_attribute_name(:name)
のように,モデルの human_attribute_name メソッドを使うこと。
「human」というのは〈人間に見せるための〉という含みだろう。
それにしてもメソッド名が長い。
このままの形だとビューがゴチャゴチャする。User モデルにはほかにも email, phone, company, address などなどのカラムがあるだろうから,ビューが User.human_attribute_name だらけになる。
そこで,だ。ビューファイルの先頭に
<% han = User.method(:human_attribute_name) %>
とか
- han = User.method(:human_attribute_name)
などと書いておけば
han[:name]
の形で使うことができ,ビューがスッキリする。
(これが良い流儀といえるかどうかは知らん)
この例は,〈固定したレシーバーとさまざまな引数に基づいて動作するメソッドを何度も呼び出す〉ケースに該当すると言えるだろう。
さいごに
-
Methodオブジェクトの出番はやっぱりProcオブジェクトより少ない - その原因は,レシーバーが固定であることや,メソッド呼び出しに使っても大して簡潔にならないことなどだろう
- 現実に使いうると言えそうな例を二つ挙げた
- それぞれ,レシーバーにとくに意味が無い例と,レシーバーに依って動作する例
追記:メソッド参照演算子(2019-06-11)
コメントで @asm さんが教えてくださったとおり,Ruby 2.7 で導入が予定されているメソッド参照演算子 .: を使えば,
User.method(:human_attribute_name)
は
User.:human_attribute_name
と書ける。
同様に,
method(:Integer)
は
self.:Integer
と書ける。この self は略せない。
なお,この Integer は Kernel モジュールのモジュール関数なのでレシーバーは Kernel にすることもできて,
Kernel.:Integer
と書くこともできる(コメントのとおり)。
rbenv の人は rbenv install 2.7.0-dev して試すことができる。
メソッド参照演算子については @hanachin_ さんが記事を書かれていた:
Ruby 2.7の新機能メソッド参照演算子 - Qiita
(読んだことを忘れていた・・・ orz)
追記:メソッド参照演算子はボツ(2019-12-03)
結局,メソッド参照演算子 .: は Ruby 2.7 に取り入れられなかった。幻に終わったのだ。
2.7.0-preview3 で既に消えている。
追記:番号指定パラメーターの出現でますます疎遠に(2019-12-25)
Ruby 2.7 では「番号指定パラメーター」と呼ばれるブロックパラメーターが導入される。
これは,ブロック内でブロックパラメーターを宣言せずに _1 とか _2 などと書けば,1 番目,2 番目のブロックパラメーターになる,というもの。
これを使えば
integer_strings.map{ |str| Integer(str) }
は
integer_strings.map{ Integer(_1) }
と書ける。
どう見ても
integer_strings.map(&method(:Integer))
より簡潔だし理解しやすい。
これで Method オブジェクトの出番はますます減るだろう。