Ruby
Haskell
Rails
プログラミング
Gem

Rubyに型を付けるRubypeというgemを作ったヨ!

More than 3 years have passed since last update.


あいさつ


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の良さを殺してない感じが好きです.

おやすみ.