スクラップ・ユア・ボイラープレート
Scrap Your Boilerplate:A Practical Design Pattern for Generic Programmingという論文を勉強中です。
以前リストの自由度で話した内容と関連しているのですが、
Haskellのリストは長さは可変な代わりに要素の型が全て同一でなくてはいけなくて、タプルは要素型が同一でなくてはよいけれども長さは固定、つまり
リストでは [1, 2] :: [Integer] [1, 2, 3, 4] :: [Integer] のように長さの違うリストでも同じ型で表現できるけど、 [1, 'a'] のようなことは許されない。 タプルでは (1, 'a') :: (Integer, Char) は許されても (1, 2) :: (Integer, Integer) (1, 2, 'a') :: (Integer, Integer, Char) といった感じで、長さの違うものは別の方になってしまう。
これでは可変長で要素型が単一でないデータはどうやって表現したらいいのかという問題が出てきます。
そこで出てくるのが、dataなどで複数の型の情報を保持する型を定義する方法:
data Variant = Int Int | Char Char | String String | …
こうやってやると、タイプコンストラクタのオーバーヘッドはありますが、リストの中に複数の型の値を混在させることができるわけですね…
[Int 3, Char 'a', String "hello"] :: [Variant]
こういった型の異なる値を保持できるデータ型はライブラリ内にもいくつかあって、Language.Haskell.Parserなどを見るとさまざまなデータ型を含むツリー構造がHsModuleという型として定義されています。
リストに色々な型の値を突っ込むことができた後にやりたくなることはといえば、そのデータをアクセスすることですよね…
例えば、リスト中にある数字を全て合計したかったら:
addIfInt :: Int -> Variant -> Int addIfInt i (Int j) = i + j addIfInt i _ = i foldr (addIfInt) 0 ([Int 3, Char 'a', String "hello"] :: [Variant]) =>3
なんて感じのことをやるわけですよね…でもこれをHsModuleのように深いデータ構造に対してやろうと思ったらaddIfIntのようなコードを大量に書かなくてはいけないですね…まぁ、不可能ではないのですが、コードが大量化しがちです。そうなってくると後でデータ構造に変更を入れたくなってもなかなかできませんね…
最初にあげた論文ではこのようにコードに繰り返し現れる、ほとんど同じだけど微妙に違うだけになかなか取り除けないコード断片のことを「ボイラープレート」と呼んでいます。そして、Data.Genericsの力によって、ボイラープレートを書かずに同等のことができるという話です。
すばらしいですね…
ちなみに上のaddIfIntの例は:
everything (+) (mkQ (0::Int) id) ([Int 3, Char 'a', String "hello"] :: [Variant])
と書くことができます。(Variant宣言にderiving (Data, Typeable)を付け足す必要があるはずです。)addIfIntは完全に省くことができちゃうわけですね…すばらしい…
さらにリストもタプル(2プルから7プルまで)もDataとTypeableのインスタンスになっているので、Variantがリストやタプルの中に入っていても上記のコードは中にもぐっていってIntだけを見つけて合計を計算してくれます。強力ですね…
[Variant]なデータ内のIntの部分にだけ関数を適用することもできます:
everywhere (mkT (+(1::Int))) ([Int 3, Char 'a', String "hello"] :: [Variant]) => [Int 4,Char 'a',String "hello"]
これは[Variant]内のIntな部分だけ(+1)を適用してくれます…
これは確かにボイラープレートを書く気がなくなっちゃいますね…
Data.Genericsの中身の解説はもうちょっと理解が深まってから…今回は利用例のみということで…
ではでは。