栗原勇樹さんインタビュー
5/13 渋谷の株式会社spice lifeにて伺いました(話し手:栗原さん、聞き手:笹田)。
栗原 実は、だいたいスライドが出来てまして。
笹田 え、本当ですか。素晴らしい。今回のご発表は、type_struct という gem を作られた経験から、Ruby の型に対する提言を頂ける、という感じになるでしょうか。
栗原 Ruby の型に対する、みたいな話は、あまり言えないかもしれません。あまり言うと怖い人に怒られるかも。
笹田 以前、type_struct gem を、Ruby のバグトラッカーで紹介 されていましたよね。たしか、Struct#new
にキーワード引数で値を渡したい、という要望だったと思います。
栗原 はい。まさに同じことをやろうとしました。ついでに、型をつける書き方がいいかと思って、遊び心で入れてみた、という次第です。でも、作ったけど使わなかったんですよね。Struct
のままでいいじゃん、となって。
笹田 なるほど。
栗原 ただ、次に JSON を扱いだしたとき、全部 Hash になってしまうので、json['foo']['bar']
のように書くのが非常に面倒だなと思いまして。タイポしても気づきづらいですし。なので、json.foo.bar
のようにアクセスしたいなと。別の言語、Go や Crystal には、Struct を定義しておくと、JSON から、その型のオブジェクトが取れる、という機能があるので、それを作ろうと。
笹田 型を指定するのと、メソッド呼び出しで書ける、というのは別々の話ですよね?
栗原 はい。その二つを組み合わせると良いかと思いまして。そこで、こういうふうに書けるようにしました。間違えた名前があるとエラーになるので、わかりやすくなったと思います。
Point = TypeStruct.new{
x: Integer,
y: Integer,
}
Line = TypeStruct.new{
p1: Point,
p2: Point,
}
hash = JSON.parse(%({"p1":{"x":3,"y":10},"p2":{"x":5,"y":9}}))
line = Line.from_hash(hash)
p line.p2.y #=> 9
笹田 ハッシュ値を、この TypeStruct の構造体に変換するんでしょうか。
栗原 はい。JSON でも YAML でも対応したいと思いまして。
笹田 どうやって型とマッチさせるんですか?
栗原 メンバーのキー名の一致と、値の ===
でマッチできれば、それを変換するようにします。例えば、Point.from_hash({x: 1, y: 2})
とすると、x
、y
というメンバがあり、Integer === 1
、Integer === 2
がマッチするので、hashからPointのインスタンスに変換できます。
笹田 必要なメンバが足りない時はどうなるんでしょう。
栗原 メンバーが足りなければエラーになります。逆に、増えた場合、例えば、{x: 1, y: 2, z: 3}
の場合は、マッチして、単に z
を無視します。
笹田 ===
でマッチはするが、from_hash
のようなメソッドを持たないクラス(型)を指定した場合はどうなるんでしょうか。
栗原 うまくいきません。そこは自己責任ということで。
笹田 なるほど。
栗原 だいたいうまいこと行くのですが、いくつか問題がありました。まずは、Array
です。型として Array
を指定することはできるのですが、そのArrayの中身の型が指定できないのです。そこで、ArrayOf(type)
というものを導入しました。これで中身の型を指定することができます。また、同じように、HashOf(type)
というものもサポートしています。
Bar = TypeStruct.new(
baz: TypeStruct::ArrayOf.new(Integer),
)
p Bar.new(baz: [1, 2, 3]) #=> #<Bar baz=[1, 2, 3]>
笹田 なるほど。
栗原 それから、Boolean
と nil
がくせ者です。true
か false
をマッチさせられて、===
の左辺に置けるものが Ruby にはないんですよね。TrueClassとFalseClassにBoolean moduleをincludeして判別しているライブラリもあります。
また、nil
を許すかどうかは、他の言語でも、色々な考え方があるようです。CやGolangでははじめからnil
を許していますが、Crystalだとnil
はデフォルトでは許しておらず、String | Nil
のようにオプションで許可しなければなりません。
笹田 なるほど。
栗原 問題を一般化すると複数の型をゆるすものだと考え、解決策としてUnion classを作りました。指定された型のうち、どれか一つにマッチすればOKというものです。
これなら「TrueClassもしくはFalseClassにマッチするもの」とすればいわゆるBooleanが作れますし、「何かのclassもしくはNilClassにまっちするもの」とすればnilがありえることを定義できます。ArrayOfなどと組み合わせて「FooもしくはBarもしくはBazの配列」も定義できます。
Aaa = TypeStruct.new(
foo: TypeStruct::Union.new(TrueClass, FalseClass),
bar: TrueClass | FalseClass # refinementsによるTypStruct::Union.newの省略記法
baz: Baz | NilClass,
qux: ArrayOf(Foo | Bar | Baz)
)
栗原 その他にも、実験作としてInterfaceもあります。Ruby だと duck typing がしたい、というのがあります。あるメソッドを持っているものでマッチするようなものを書けるようにしました。
Foo = TypeStruct.new(
bar: TypeStruct::Interface.new(:read, :write)
)
Foo.new(bar: $stdin)
Foo.new(bar: 1) #=> TypeError
笹田 色々ありますね。Unionの場合、複数の型がマッチするような場合はどうなるんですか?
栗原 それは直面した問題です。Unionに設定されているいずれかの型がマッチすればその型で変換を試みるのですが、これだと最初にマッチしたものになってしまうので曖昧さがありますし、もし型が間違っていたとき、どこが本当に間違いなのかわかりづらくデバッグもしづらいです。Crystal だと、JSON に対してUnion型を指定するのはサポートしていません (JSON mapping, mixed type array #1061)。あつかいが難しいのかな、と。Union が本当にデメリットよりメリットが大きいのか、まだ答えは出ていません。現在ではデバッグ用の出力をリッチにするという方法で逃げています。
笹田 で、Ruby 3 の話につながる感じですか。
栗原 笹田さんの以前の発表 『MRI Internals』(PDF) を引いたり、他の言語からヒントが得られないか、検討しようと思っています。例えば、JavaScript ですと、後付けで型を付ける文化があると思うのですが、そういうのを調べようと思います。
笹田 Ruby に型があると(型を書くと)便利だと思いますか?
栗原 便利は便利だと思います。実際に仕事でも使っているのですが、新人に「この JSON はどういう構造がありえるんですか?」と聞かれた時に「ここに型定義がまとまっているよ」とドキュメント的に示すことができます。書いていても安心感がある。
ですが、色々考えないといけないことがあると思っています。そういえば、Ruby 3 が目標としているのは、事前の静的なチェックですよね。
笹田 はい。ただ、まつもとさんが型を書かせたくない(書きたくない)、と言っているのがどうなるのかな、と。組込みのCで書かれたメソッドには(処理系開発者ががんばって)型を書く分には問題無いけど、とは言ってました。
栗原 あ、そこが一番問題だと思っていました。それが解決できるのはいいですね。(大変そうだな……)
笹田 Crystal は、そういう情報から、すべて推論すると思います。しかし、空の配列などには型を書かないといけないんですよね。まつもとさんは、そこが気に入らないらしい。
栗原 型を書きたくないなら、というアイデアがあって、最初は型が決まらない状態から初めて、最初にきたものは許すけど、二度目からは、その型しか許さない、というようなものを作ってみました。GuessStruct。使ってはいないのですが。
笹田 同じようなアイデアで、テストできた型以外は弾く、というのはどうだろう、というアイデアがありました。テストが型だ、みたいな。ただ、テスト書く方が長いという。
栗原 まつもとさんは、テストも書きたくないって言ってましたよね。
笹田 言ってました。
栗原 やはり、型が書けると、開発時に便利だと思いますね。
笹田 わかります。ぜひ、まつもとさんを説得して下さい。
栗原 「型書いてもいいじゃないですか」って。
笹田 今日は、お忙しい中ありがとうございました。