2020/12/27追記
https://www.ruby-lang.org/ja/news/2020/12/25/ruby-3-0-0-released/
=> Ruby3.0.0にRBSとTypeProfと用いた静的解析に対するソリューションが提供されました。
今後はこちらのエコシステムをガンガン盛り上げていきましょう💪
#あいさつ
P.S. 12/11 HaskellからRubypeにgemを変更しました
こんにちは.
寒さが一段と厳しくなってきた事もありまして心を温めるGemを作りました.
Rubyの振る舞いを汚染する事無く型保証の恩恵をゆるふわ受けられるgemです
(型をつけるとか型保証という言葉をこの文脈ではメソッドの引数返り値のクラスを実行時にチェックするという意味で使っている.)
得体の知れないGemで抵抗感があるかもしれませんが、コード自体は50行以下の薄いGemなので気軽にさくっと副作用を避け型保証を導入したい方に強くオススメします.
Giuhub
Rubygems
いつも通りのメソッドにプラガブルな型情報を付け加えるだけで引数と返り値の型を保証する事が出来ます、型情報の付加を除くメソッドの記法やふるまいはあなたの知っているRubyのままです.
(型なしメソッドと型ありメソッドの共存も可能)
MatzことRubyの作者の松本さんが3.0(次世代Ruby)での型導入を示唆されていましたが、イメージがあまりつかなかったのでどんなもんかgemレベルで試せたらいいなと思いました.
適当なRailsアプリに導入してみましたがイイ感じに動いています(ドッグフード)
雰囲気
gem install rubype
などでgemを持ってきて(Ruby2.0.0+を必要とします)
p.s. 2014/12/07 11:00(JST) シンタックスを大幅に変更しました.
(もはやHaskellの面影なし..)
require 'rubype'
# 引数の型チェック
class MyClass
def sum(x, y)
x + y
end
typesig sum: [Numeric, Numeric => Numeric]
end
MyClass.new.sum(1, 2)
#=> 3
MyClass.new.sum(1, 'not numeric')
#=> ArgumentError: Wrong type of argument, type of "not numeric" should be Numeric
# 返り値の型チェック
class MyClass
def wrong_sum(x, y)
'string'
end
typesig wrong_sum: [Numeric, Numeric => Numeric]
end
MyClass.new.wrong_sum(1, 2)
#=> TypeError: Expected wrong_sum to return Numeric but got "not numeric" instead
説明
Module
クラスに定義してあるtypesig
メソッド用いて既存のコードにあるメソッド定義に型情報を記述するだけです.
# rubype gemをrequire
require 'rubype'
# いつも通りのメソッド宣言の後に型情報を加える.
class MyClass
def sum(x, y)
# いつも通りのRubyコード
end
typesig sum: 型情報
end
型情報の記述方法
# 引数が1つのメソッドなら
# (第一引数のクラス) => (返値のクラス)
例. Numeric => Numeric
# Numericクラスのオブジェクトを引数にしてNumericクラスのオブジェクトを返すメソッドである事を保証する
#引数が2つのメソッドなら
#(第一引数のクラス), (第二引数のクラス) => (返値のクラス)
例. Numeric, Numeric => Array
#Numericクラスのオブジェクト2つ引数として取りArrayクラスのオブジェクトを返すメソッドである事を保証する
#引数が3つのメソッドなら
#(第一引数のクラス), (第二引数のクラス), (第三引数のクラス) => (返値のクラス)
例. Numeric, Numeric, Numeric => Array
#Numericクラスのオブジェクト3つ引数として取りArrayクラスのオブジェクトを返すメソッドである事を保証する
...
例. 人と結婚するのは人だけ
People
オブジェクトのmarry
メソッドが引数にPeople
オブジェクトを取る事を保証する
class People
# Anyクラスはどんなオブジェクトでもマッチする.
def marry(people)
# 素敵な実装
end
typesig marry: [People => Any]
end
People.new.marry(People.new)
#=> 1
People.new.marry('non people')
#=> ArgumentError: Wrong type of argument, type of "non people" should be People
推したい所
ありのままのRubyに副作用の少ない形で導入出来る事が特徴です.
振り切ってネタGemにしてもよかったのですが、なくなるべく本番レベルで使える事を念頭におきました.
既存のコードはそのまま動きます.(Module#typesigを定義してなければ)型情報を付けたいメソッドにのみ型情報を付けてください.
コンパイル時に型チェックとか出来ないけど
コンパイル時に型チェックしてエラー検出という恩恵は現時点では受けられませんが、
メソッド内での引数のクラスチェックなどの記述は省かれ、またエラーが自明になると思います.
ドキュメントとしての役割も果たせます.
ダックタイピング
返り値の型(クラス)は保証したいけど、引数はちょっと..というあなたのためにAny
クラスを用意してあります.
どんなクラスのオブジェクトとでもマッチするようになっております.
class MyClass
def any_meth(any_obj)
# 素敵な実装
return 1
end
typesig any_meth: [Any => Numeric]
end
MyClass.new.any_meth(1)
#=> no error
MyClass.new.any_meth('string')
#=> no error
これからやりたい事
可変長引数, キーワード引数の対応
def sum(*ary)
ary.inject(:+)
end
typesig sum: [[Numeric] => Numeric]
エラーハンドリング
型宣言と実際の引数の数が合わないとかはメソッドの宣言の時点で知らせたい.
実行前の型チェック
型シグネチャーがついているメソッドを静的解析する
(たとえば#to_iが呼ばれていたらそれは#to_iが定義されているクラスだみたいな)
仕組み
各Module
クラスのオブジェクト(いわゆるモジュール, クラス)に@__haskell__
という名無しモジュールを持たせてそいつに型チェックを含んだメソッドを付けてprepend
しています.
イメージ的には@__haskell__
という型チェックをする薄いモジュールを通して各メソッドを読んでいる感じ.
(type
で宣言しなかった普通のメソッドはこの限りでなく、普段通りの振る舞いを辿る.)
感想
手前味噌ながら、適当に使ってみましたが結構イイです.
メソッドの引数, 返値を実行時に調べるだけのゆるふわ型保証ですが、
それがまたイイです. Rubyの良さを殺してない感じが好きです.
おやすみ.