class String def sand(x) x + self + x end end
これは一見うまく動くようだが、
"hello".sand("'") # => 'hello'
実は文字列を指定しなければエラーとなる。
'hello'.sand(1) # => # ~> -:2:in `+': String can't be coerced into Fixnum (TypeError) # ~> from -:2:in `sand' # ~> from -:5
原因は、xに渡された1(Integer)の+メソッドは、String型を渡すとTypeErrorを投げるから。文字と数字が足せないのは当たり前なので、妥当だといえる。
このようなエラーを実行前にトラップするために、静的型付けが有用だとされてきたが、事実Rubyではこの初歩的なバグは実行時ならないとわからない。
もちろん言語仕様上コンパイル時にエラーを上げることなど不可能なので、エラー処理をわざわざ入れるというのはよくやることだ。ここでは、sandを「エラー時にはnilを返す」ようにする(ここは、例外を投げるようにしてもいいかもしれないが、とりあえず)。
class String def sand(x) if(x.is_a?(String)) x + self + x end end end
これで、Integerを引数に指定するとnilを返すようになる。しかし、このような煩雑な処理を毎回書いていると冗長になるし、オブジェクト志向言語では、型ではなくて「特定のメソッドが定義されていれば」など込み入った定義もしたくなる。そこで、これらをできるだけ簡単に判断できるようなユーティリティを書いてみた。
def type_check(args, &proc) check_function = lambda{ |val, check| if check.nil? nil elsif check.respond_to?(:call) check.call(val) elsif check.is_a? Array val.__send__(*check) elsif check.is_a? Module val.is_a?(check) end } error = args.find{ |a| not(check_function.call(*a)) } if(error) warn "argument error: #{error[0].inspect} is not passed #{error[1].inspect}" nil else proc.call if proc end end
関数type_checkは、すべてのチェックにパスしたら渡されたブロックを実行してその結果を返す関数。いずれかのチェックに失敗したら、エラーを出力してnilを返す。
チェックは、値 => 判定式 のような連想配列もしくは[[値,判定式],...]のような配列。
使える判定式は、
- nil ... 常にパス
- [symbol, *args] ... 値.send(symbol, *args)がtrueを返せばパス
- lambda{|x|} ... 値を引数に取る無名関数(厳密にはcallメソッドが定義されているもの)。trueを返せばパス。
- Module ... その型とis_a?関係ならパス
例えば以下のように使う。
type_check([[1 , Integer], [2 ,[:is_a?, Integer]]]){ :passed } # => :passed
これを利用すれば、sandは以下のように定義できる。
class String def sand(x) type_check(x => String) do x + self + x end end end
利点は、 x => Stringは、「xはString」のように自然言語に読み替えやすいことか。このままでは微妙なので、型チェックを伴う無名関数を定義するメソッドを定義してみる。
def tclambda(*args, &proc) lambda{ |*a| if proc.arity >= 0 if proc.arity != a.size raise ArgumentError.new("wrong number of arguments (#{a.size} for #{proc.arity})") end else if -(proc.arity+1) > a.size raise ArgumentError.new("wrong number of arguments (#{a.size} for #{proc.arity})") end end type_check(a.slice(0, args.size).zip(args)){ proc.call(*a) } } end
type_checked_lambdaは、lambdaとほぼ同じように使うが、ブロック以外に引数を取る。それは、各引数のチェック式になる。
例えば、配列aryの中の数を2倍にして返したい場合、以下のようにできる。
ary.map(&tclambda(Integer){ |x| x * 2 })
こうすれば、aryに数値以外がはいっていれば、そこはnilとなる。かなりすっきり書けた。
これがあれば、型をチェックするdefを定義することもでき、それを使えばsandの定義はかなりすっきりする。
class Object def self.defun(method_name, *args, &proc) define_method(method_name, &tclambda(*args, &proc)) end end class String defun(:sand, Integer) do |x| x + self + x end end
このように、やや強引ではあるが、型を定義したメソッドを定義できた。できるだけこれでメソッドを定義すれば、エラー箇所がより明確になるかもしれない。
動機
関数、とくに自分の書いたものは信用してしまいがちで、この類のエラーは後を立たない。今回は、静的型付け言語の引数チェックを取り入れられないかという実験の一環でコードを書いてみた。
こういったメタなコードを書くたび、Lispへの憧れが深まっていく(笑)。