言語ゲーム

とあるエンジニアが嘘ばかり書く日記

Twitter: @propella

Haskell でダックタイピング 、色々な型を混ぜて配列を作る。

動的型言語の特徴の一つに、配列に色々な型のデータを混ぜられる事があります。いわゆるダックタイピングというやつです。例えば Javascript では ["1", 2, 3.4] のような配列を作る事が出来ます。Haskell のような強い型付けのある言語でこれを実現する方法を勉強したので書きます。

目標として、配列の中にあるデータを文字列として連結する事にします。Javascript で言うと、

append = function (xs) {
  var result = xs[0];
  for (var i = 1; i < xs.length; i++) result += xs[i];
  return result;
}

のような関数 append を作ります。append(["1", 2, 3.4]) の答えは "123.4" になります。

代数データ型を使う

Haskell では直接リストに別の型のデータを入れる事が出来ないので、ある型で包んで入れる事にします。多分一番素直な方法は代数データ型を使う事だと思います。

data Val0 = S String
          | I Int
          | F Float deriving (Show)

これで色んな型を配列に入れる事自体は出来ます。

*Main> [S "1", I 2, F 3.4]
[S "1",I 2,F 3.4]

文字列の連結をするには、一旦内容を文字列に変換すれば良いです。任意のデータを文字列に変換するには show という関数が使えます。

toString0 (S x) = show x
toString0 (I x) = show x
toString0 (F x) = show x

append0 :: [Val0] -> String
append0 [] = ""
append0 (x:xs) = toString0 x ++ append0 xs
*Main> append0 [S "1",I 2,F 3.4]
"\"1\"23.4"

ちょっと Javascript と実行結果が違うけど、説明の都合なので気にしないで下さい。

Existential type

さて、上で定義した Val0 をよく見てみます。Haskell では String, Int, Float 以外にも沢山型があります。もしも他の型に対応しようと思ったら単に Val0 に適当に型定義と toString を追加すれば良いだけですが、なんか馬鹿馬鹿しい感じもします。show が定義されている型なら同じ方法で文字列に変換出来るので、わざわざ新しく作らなくても良い方法がありそうな物です。 Existential type を使うとそれが出来ます。 Existential type を使うには、{-# OPTIONS -XExistentialQuantification #-} というオプションを付けます。

上の例では、S や I 等のコンストラクタを使い型ごとに toString で文字列の変換方法を指定しました(結局全部 show しただけですが)。次の例では、forall a. (Show a) => という書き方によって単一のコンストラクタ Val の後に、Show 型クラスのオブジェクトなら何でも使えるという指示をしています。この forall a. という表記は冗長な気がするんだけど何で要るんだろう。。。

data Val = forall a. (Show a) => V a

toString :: Val -> String
toString (V x) = show x

append :: [Val] -> String
append [] = ""
append (x:xs) = toString x ++ append xs
*Main> append [V "1",V 2,V 3.4]
"\"1\"23.4"

というわけで、案外簡単に色んな型を含む配列を作る事が出来ると分かりました。

GADT

もう一つのやり方として、GADT というのもあります。GADT を使うには {-#LANGUAGE GADTs #-} を付けます。GADT では、コンストラクタが受け付ける型を関数の型のような形で定義します。文法的にはこっちの方が分かりやすい気がします。どうやって使い分けるんだろう。

data Val1 where
          V1 :: Show a => a -> Val1

toString1 :: Val1 -> String
toString1 (V1 x) = show x

append1 :: [Val1] -> String
append1 [] = ""
append1 (x:xs) = toString1 x ++ append1 xs
*Main> append1 [V1 "1",V1 2, V1 3.4]
"\"1\"23.4"

まとめ

Haskell で配列に色々な型のデータを入れるには、代数型、Existential type、GADT のどれかを使えばいいです。ソースは https://gist.github.com/955a85be2ea40b8fd2fc にあります。