僕はHashが嫌いだ。
できるだけHashは使いたくない。
h[:foo]
とか
h.fetch(:foo)
とか本気で使っているのかね君たちは(今日は何となくこんな感じですすみませんすみません)。
Hash
Hashはとにかくデバッグが面倒だ。
設定やら多値を表現するためか生存が長くなりがちなくせに、 どこで生成されたHashなのか、どこで追加されたkeyなのかよくわからなくなる。
class名がないから何を表したいオブジェクトなのか、デバッグした時にわからない。class名でgrepもできない。
JSONを扱うプログラムではついついHash#[]
やHash#fetch
ばかりのコードになりがちで、
エラーが起こったらHashのkeyをtypoしたんだか値がnil
なんだかよくわからなくなる。
Hash#fetch
でkeyを間違えるとKeyError
が発生することで検知できるけど、「どんなkeyがあったかな?」と思っても正しいkeyは自分でコードを読むかp h.keys
するなりして探さないといけない。
いまのところdid_you_mean gemもKeyError
に対応していないので、typoにおびえてコードを書くか、ひたすらデバッグするかだ。
*1
なにより
h[:foo] h.fetch(:foo)
は長い。
Hashはどんなkeyが来るかわからない時だけ使うべきだ。 あと、「記述が少ない」「名前を覚えなくていい」という利点から、keyword argumentsのような引き数にも便利。 gemの外部APIとしては十分選択肢だと思う。
Struct
Structは好きだ。
どこで生成されたオブジェクトなのかclass名でgrepすればだいたいわかるし、key名を間違えてもNoMethodErrorで教えてくれる。 なんならdid_you_mean gemが正しそうな名前を教えてくれる。
「どんなkeyがあったかな?」と思ったらclassの定義を読めばいい。
s.foo
の方がシンプルだ。
golangやcrystalのようにJSONを特定のclassに属するオブジェクトに変換する方法があれば、Hash的な使い方でなく、Struct的な使い方ができる。
TypeStruct
ようやく本題。
以前、keyword argumentsが使えてより厳しいStructということでTypeStruct gemを作った。
これをさらに推し進めて、hashからマッピングできるように実装してみた。
README.mdから引用すると、
Point = TypeStruct.new( x: Integer, y: Integer, ) Color = Struct.new(:code) Line = TypeStruct.new( start: Point, end: Point, color: Color, ) hash = JSON.parse(%({"start":{"x":3,"y":10},"end":{"x":5,"y":9},"color":{"code":"#CAFE00"}})) line = Line.from_hash(hash) p line #=> #<Line start=#<Point x=3, y=10>, end=#<Point x=5, y=9>, color=#<struct Color code="#CAFE00">> p line.start.y #=> 10 line.stort #=> NoMethodError
このように、key名とclassを設定すれば、HashオブジェクトをStruct(っぽい)オブジェクトに変換してくれる。
設定に違反するデータならエラーを出してその場で教えてくれるし、参照先のclassがTypeStruct
から作ったclassなら再帰的にオブジェクト化してくれる。
Structオブジェクトにも対応しているけど、Structの場合は値のclassを設定できないのでなんでも入れられて再帰的にオブジェクト化できなくなるが、既存のcodeで使いやすいようにつくってみた。
2つの記法
「値のclassを設定できる」を実現するために2つの記法を用意した。
Union
たとえば「true
かfalse
になるメンバをつくりたい」と思うと、Rubyの場合困ったことになる。
Foo = TypeStruct.new( bar: TrueClass, # or FalseClass??? ) Foo.new(bar: false) #=> TypeError
TrueClass
とFalseClass
に判別用のmoduleをincludeすればチェックできるけど、標準classを書き換えたくない人も多い。
そのため、独自のclassを用意してみた。
Foo = TypeStruct.new( bar: TypeStruct::Union.new(TrueClass, FalseClass) ) Foo.new(bar: false)
標準classを少しなら拡張してもよいという方のために、こんな書き方もできるようにした。
require 'type_struct/ext' using UnionExt Foo = TypeStruct.new( bar: TrueClass | FalseClass ) Foo.new(bar: false) Foo.new(bar: true)
Class#|
メソッドを用意して、classをつなげて複数のclassを表現できるようにした。refinementsを使っているので、Class#|
が有効になる範囲も限定できる。*2
ArrayOf
メンバを配列にしたい場合もあるだろう。
Cだと
struct foo { int ary[]; };
golangだと
type Foo struct { ary []int }
みたいなやつだ。
その場合の表現方法も考えて、専用classを用意した。
Foo = TypeStruct.new( ary: TypeStruct::ArrayOf.new(Integer) ) Foo.new(ary: [1,2,3])
名前が長いのが玉にキズなので、これも短縮できるようにしている。
require 'type_struct/ext' Foo = TypeStruct.new( ary: ArrayOf.new(Integer) ) Foo.new(ary: [1,2,3])
組み合わせ
もちろんUnionとArrayOfは組み合わすことができて、「配列の時もあるけどnilのときもあるメンバ」を表現できる
require 'type_struct/ext' Foo = TypeStruct.new( ary: ArrayOf.new(Integer) | NilClass ) Foo.new(ary: [1,2,3]) Foo.new(ary: nil)
これらの記法を組み合わせれば、かなり複雑な構造を持ったJSONでも、特定の名前のついたclassのインスタンスオブジェクトを作ることができるようになるので、お試しください。