Edited at

Rubyのlambdaを型付き言語としてパースして扱う実行時型検査ライブラリを作った

More than 1 year has passed since last update.


作ったもの

Rubyのなんちゃって実行時型チェッカを作りました。(https://github.com/rike422/moguro)

こういう感じでかけます。

class MockClass

include Moguro::Decorator

# puts_methodの引数をチェック
pre_c ->(a: Integer, b: String) {
assert_equal(a, 1)
assert_equal(b, 'b')
}

def puts_method(a, b)
puts b
a + 1
end

# post_contract_violation_methodの戻り値をチェック
post_c -> (first: String) {
p first
}
def post_contract_violation_method
1
end

end

c = MockClass.new
c.puts_method(1, 'b')
# 1
# => 2

begin
c.puts_method('a', 1)
rescue => e
p e
end
# =>
# Type MissMatch: a is expected (Integer) actual a(String) (Moguro::Errors::ArgumentsTypeMismatchError)
# Expected: [a: (Integer), b: (String)]
# Actual: [a: a(String), b: 1(Integer)]
# Value guarded in: MockClass::puts_method
# At: #{source_location}

begin
c.post_contract_violation_method
rescue => e
p e
end

# =>
# Type MissMatch: first is expected (String) actual 1(Integer)
# Expected: [first: (String)]
# Actual: [first: 1(Integer)]
# Value guarded in: MockClass::post_contract_violation_method
# At: #{source_location}


上位互換(使うべきもの)

ちょうど概ね完成したあたりで、sorbetが発表されたのでなんとなく悲しい気持ちになりました。


思いつきのもと

flowtypeをつかってjsを書いていたところあることに気づきました。


型付き関数

const lambda = (a: string, b: number) => {

}


rubyのオプション引数つきlambda

lambda = -> (a: String, b: Integer) {

p a
}

完全に一致してますね。

さらにUnion Type, さらにArray Typeも同じノリでできそうです。


Union Type


uniontype

const lambda = (a: string | number) => {

}


エセuniontype

lambda = -> (a: String || Integer) {

p a
}


Array Type


ArrayType

const lambda = (a: Array<number>) => {

}


エセArrayType

lambda = -> (a: Array[String,Integer]) {

p a
}

つまりrubyのオプション引数の部分に型を書いたら、ts・flowtypeユーザにとっては割と違和感なく

型がかけるということです。

これに気づいた私は矢も盾もたまらずといった感じで、社内LT用に型チェッカを作ることにしたのでした。


すごく参考になったライブラリ


実装時に使った・作ったもの


Decorator

オプション引数で型をつけるといっても、本来の用途で使えないのであれば意味がありません。

そこでDecoratorのスタイルで型を定義できるようにしようと思い立ちました。

こういうときRubyは最高で、動的にModuleクラスのインスタンスを作成し、

受け取ったlambdaを実行したあとsuperを呼ぶ同名のメソッドを定義してあげて、本来のクラスにprependするだけで実装できます。

この辺でやってます

- https://github.com/rike422/moguro/blob/master/lib/moguro/handlers/method_handler.rb#L23


Lambdaのコードを再解釈

今回はクラスメソッドで受け取ったlambdaのオプション引数部に書いてあるクラスを、

型として扱いチェックを行わなければならないので、lambdaのコードを再解釈する必要があります。

その際に、とても役に立ったライブラリが下の2つです。


method_source

rubyのコア機能だけだとsource_locationでの定義場所しか取れませんが、

method_sourceを使うことによりソースコードをまるっと取れます。

最高です。


パーサ


実装法

ざっくりというとmethod_sourceで受け取ったlambdaのソースコードをパースして、

キーワード引数のイベントハンドラ(on_kwoptarg)で受け取ったものを、

更に引数解析用のパーサに突っ込むことで、定数評価などのイベントが発生し型定義を取得することができます。

受け取った型に応じて、型チェック用のクラスのインスタンスを保存しておき、method.parametersで受け取った引数名をキーにして、値を型チェッカに渡しています。


Assert

開発開始あたりで、t-wadaさんのスライド

を見直していて、このGemを使って事前条件と事後条件のチェックを行えば良いんじゃないかと思い立ちました。

現在は直接includeされたクラスにTest::Unit::Assertionsを突っ込んじゃっていますが、

理想としては対象のオブジェクトの状態をコピーしたサンドボックス用のクラスを作ってあげて、

そのクラスにTest::Unit::Assertionsをincludeしておいてというのが理想です。


パフォーマンス

測ってないですが相当落ちると思います。

Moguro.enabled = false

対象クラスの読み込み前にを実行してあげれば、上記の解釈などの処理は行われません。


まとめ

みなさんもrubyのシンタックスを再定義していきましょう!!!


名前の元ネタ

ドーン