Rubyにとってのメソッド引数のタイプチェックとは
JavaとかObjective-Cとか静的な言語ではメソッドの引数やクラスのインスタンス変数などは予め型を指定しておく必要があって、その型に合わない引数が渡されると、コンパイル時にエラーになることによって、そのメソッド内では安心して引数にその型にしかわからないメッセージを送ることができる。
Rubyはメソッドの引数やインスタンス変数の型チェックは行わないで、ゆるゆるでなんでも受け入れる代わりに、メソッド内で引数に対してconversion methodsと呼ばれる型変換メッセージを送った上で、変換された引数に対して安心してその型にしかわからないメッセージを送る。
例えばto_sで、なんでもかんでもStringに変換できるとかだと魔法みたいだけど、なぜこれが実現できているかというと、ほんとに何でもかんでもいろんなコアクラスにto_sが実装されているから実現できている。
何でもかんでもにto_sが実装されている
とはいえとはいえコンパイラチェックが効いている言語と比べると、1段階甘い「安心」であるような気がする。 言語として強制的にコンパイラチェックを通して、引数の型が保証されているのに対して、Rubyでは何でもかんでも入ってきた有象無象に対して、「to_sとかって意味わかります?」って聞いて、返事が返ってくれば「この人Stringっぽいよ」ってなって、Stringとして扱い始める。
なぜ不安なのか
不安な点を列挙してみると、
①to_s理解できないパターンがある
②to_sで変換した結果が、何になるのかよくわからない(そのオブジェクトに任されている)
③メソッド使う側として考えると、そのメソッドの中でちゃんとコンバージョンメソッドとかでチェックしてくれているか不安。特にサードパーティコードから受け取った値をサードパーティコードにまた渡すような場合は、超不安になる。(例:Railsでコントローラから受け取ったparamsを直接Modelのwhereとかの引数で渡すとか)
④nilに意味を持たせたい場合でも、nil.to_sすると""になってしまうので、正常なstringを受け取っているケースと同じフローにのってしまう ⇛なんでもかんでもコンバージョンさせていいのかな問題 例:Railsでコントローラでparams受けて、Modelをフィルタリングしたいんだけど、hashであるparamsにキーがない場合は特定の動きさせたいとか
そしてテスト依存症に
to_sとかでコンバージョンさせているものの、①②とかでどっかでエラーになるんじゃないかとか思い始めると、nilとかto_sがなさそうなクラスとかを引数にするテストとかをわざわざ書きたくなってしまって、開発効率がめっちゃ落ちてる気がする。
「そんな自分を変えたいと思って、この記事を書きました」
一個一個不安を自分で回答していってみようと思う。
①to_s理解できないパターンがある
①は、to_s理解できない時点でno method errorになるはずなんで、静的な言語のコンパイルの時のエラーが実行時に出てきたと考えれば静的な言語と同じこと
②to_sで変換した結果が、何になるのかよくわからない(そのオブジェクトに任されている)
②は、コンパイラチェックでも型のチェックはしている一方で、その内容がなにかまではチェックしてないので、静的な言語と同じこと
人を信じましょう。なんなら、あらかじめメソッド利用側でコンバージョンメソッド呼んどけば
④nilに意味を持たせたい場合でも、nil.to_sすると""になってしまうので、正常なstringを受け取っているケースと同じフローにのってしまう
to_sの前にnil?で判定すればOK。コンバージョンメソッド使うとなると、「せっかくどんなクラスでも魔法のように意図したクラスに変換できるんだから、その引数が関わるフローも魔法のように画一であるべきだ」みたいな欲がでるんだけど、そもそも意味を持たせたいという意図がはっきりしてるんならシンプルに分岐させればOKだと思う。
nilに意味を持たせたいケースなんぞあるかというと、hashをメソッドの引数にとったりすると、「特定のkeyがない」という状況を判定したいケースがある。
引数でユーザIDを渡された場合には、ユーザに紐付いたギャグを取得し、ユーザIDがないか、指定されたユーザIDでギャグが見つからない場合にはランダムにギャグを返すというActiveRecordモデルのメソッドを考える。
class Gag < ActiveRecord::Base belongs_to :user def self.get_suitable_one(args=Hash.new) # self.with_user_id(args[:user_id].to_s) return self.random_scope if args[:user_id].nil? || self.with_user_id(args[:user_id]).empty? self.with_user_id(args[:user_id].to_s) end end
to_sしておけばstringを渡せるので、とにかくto_sしてActiveRecordにあとは任せればいっか!的なノリになるんだけど、(そして多分空のstring渡してもいい感じに意図通りにしてくれそうだけど)、サードパーティコードが想定してるかどうか微妙なパターンを作ってしまうのは不安・・・というケースで、to_sする前に意味を持ったnilを調べてやれば不安解消になる。