Ruby3に型は突っ込めるのか?の記事の続きですが、method_missing内部は、複数のメソッドの実装を実質載せているので、通常の型の記述方法では実現できないので、別途研究が必要になります。
method_missing
内部の実装を完全に型推論する方法は自分でも色々考えたのですが、これは難しいです。
rdlの実装では、メソッドの記述の前に型を宣言しますが、method_missing
は、たいてい内部で、super
を宣言します。
継承元で、またmethod_missing
が宣言されている場合、そこの型をどう知れば良いでしょうか?
結局良い手段がないのであれば、そうであればだいたいの場合は推論できる様な記述方法を考える必要があります。
ですが、だいたいの場合、というのは何を基準としてのだいたいでしょうか?
まずは、それを知るために、method_missing
の使われ方を調査してみましょう。
取得方法
サンプルは、以下の基準でコードを収集して行いました。
- bestgems.orgでランキングされている人気のgemで、ダウンロード数Top 500のコードをサンプルにする
- 各々のgemの中のlibディレクトリ以下を調査の対象とした。つまりはテストのコードは集計から排除した
- 動的な言語なのでどのクラスに属しているかの推論は諦めて、あくまでメソッド名の集計結果とした。
method_missingの使用状況
110ファイル、114回の定義を確認を見つけました。これを一つ一つ使われ方を確認しながら分類、整理していきました。
使われ方
目についたのはsuper
メソッドとsend
、__send__
メソッドの使用回数の多さ。
メソッド名 | 使用回数 |
---|---|
super | 74 回 |
send | 76 回 |
他の場合も、メソッド名をハッシュのキーとして利用している場合も多く、何所かしら他のクラスに仕事を移譲している場合が多い様です。
ActiveRecordのfind_by_XX
のメソッドの様に名前から機能を生成するものと思っていたのですがどうも違うようです。
実際に使われ方を幾つか観察した結果。以下の3つの者が、代表的な使われ方なのではないかという結論になりました。
-
getter
、setter
の自動生成 - ハッシユのラッパー
- 移譲の自動化
実際に、ここからは例を挙げて見ていきましょう。
名前からsetter, getterを自動生成する
与えられた名前から、setter、getterを自動で作成するコードです。
これは実際に使われているコードを見てみるのが一番実感がわくでしょう
def method_missing(meth, *args, &block)
if meth.to_s =~ /=$/
self[meth.to_s[0..-2]] = args.first
elsif instance_variable_get("@#{meth}")
self[meth]
else
super
end
end
実際の案件ではどのようなインスタンスメソッドが必要かわからない場合にはこの手法は有効です。
これらの使われ方の場合、クラスの中を見ただけでは、setter、getterに何の型が必要かの推測は不可能です。
呼び出し側のコードを解析すれば分かるかもしれませんが、これも簡単ではないです。
呼ばれる順番でデータの型がある可能性を考えないといけないですから。
- getter よりも先にsetterが先に呼ばれるという保証がある
- setterで入力されるデータ型は一貫している
これらの条件を満たしているなら、呼び出し側のコードを解析することで計算量がかかりますが実現は可能でしょう。
ここからは実感の話になりますが、大抵のコードはこの条件を満たしていると思います。
なので、setterで与えられた値かnilを返せばよいのではないでしょうか、実際やるかどうかは、RDLの今後の展開次第でしょう。
ハッシュのラッピング
getterやsetterを自動生成するパターンですが受け側がハッシュになっているパターンです。
getterのみの生成だと下のコードになります。
def method_missing(name, *args)
return @data[name.to_s] if @data.key? name.to_s
super
end
これの場合、呼び出し元のコードを全て調べないと型の推論は出来ません。
これもハッシュでデータを保存しているだけで、getter
、setter
を自動生成しているのと本質は同じです。
型の推論は基本的に不可能ですがsetter
、getter
の生成の個所次第でしょう。
外部への処理の移譲
これは分かりやすいですね。
実際に使われていたコードを見てみましょう。
def method_missing(name, *args, &block)
return super unless @view.respond_to?(name)
@view.send(name, *args, &block)
end
これらのパターンの場合、移譲先のクラスが判明している場合、呼び出しのタイミングで必ず名前判明するるので、移譲先のクラスの定義がされる保証があるかどうかが話の焦点になります
ここまででパターンが出たので
あとは、実際にはどれくらいの割合で含まれているかを数値に出してみるしかないです。
62か所、65ケース(一つのmethod_missingで分岐して2つの仕事をしていたのがあります)
総数 | |
---|---|
外部への処理の移譲 | 29 |
名前からsetter, getterを自動生成する | 13 |
ハッシュのラッピング | 6 |
その他 | 15 |
method_missing
は意外なことですが、実際にgemで使われている現場では、4割以上が、移譲の自動化にProxyクラスを簡潔に書くために使われています。
他30%が、setter、getterを自動的に生成する事、ここまでで7割以上のユースケースが定まっています。
最初は、find_by_XXXX
のメソッドの様に名前から機能を生成するものと思っていましたが
実際に、型が推論できるかどうかを表にすると下の様になりました。
外部への処理の移譲 | 型が分かる | 21 |
型が分からない | 8 | |
名前からsetter, getterを自動生成する | 型が分かる | |
型が分からない | 13 | |
ハッシュのラッピング | 型が分かる | |
型が分からない | 6 |
結論から言うと、移譲先の分かっているProxy以外は型の推論は簡単ではない様です。
あえて、呼び出し側のコードを良く解析するのであれば道は変わると思いますが、そこは仕組みが一気に複雑になるのを理解して行うべきでしょう。
どうしましょうか、method_missingは実際には、だいたいの場合にはかなり深くコードを解析しないと型は推論できないという結論になりました。
実際には、method_missingを呼び出している箇所を一つ一つ型指定していくか、method_missingのコード自体を徐々に排除していくかはさらに研究を必要とします。
また、研究を進めたらこの記事を改定して行きますのでお待ちください。