Edited at

ElmでPhantom Type (幽霊型)入門


はじめに

elmは静的型付け言語です。コンパイラで静的にコンパイル時に、型チェックにすることによって、コードの安全性(型安全)が担保されます。このような型の存在のおかげて、プログラムに間違った値が関数に適用されていないかチェックすることができます。

このような「型」とはelmをある程度つかっているユーザであれば、別の見方をすれば「値の限定」と見なすことができるというのはすぐ理解できるんじゃないかと思います。例えば、String型やInt型の区別は、Intという値に限定するという見方ができるという見方をすることができます。型というのは、とある値の集合であり、そのある値が集合に属しているか属していないか?という確認のためのものだと言うこともできるでしょう。

さて、型は「値の限定」と言ったり「値の集合」と言ったりしましたが、たとえばString型などを、「もっと限定」したり、「値の部分集合」をとるにはどうしたら良いでしょうか?

前回の記事で、実は、Neverの記事にて、その一例を紹介しています。Neverを使うことによって、ある幅が広い型を限定しているという見方ができるのではないかと思います。たとえば,Maybe Neverなどは、絶対にNothingになるように、限定しており、Maybe aの型の部分集合を取っているとも見なせると思います。

そのような値の部分集合を得るためのデザインパターンとして、Phantom Typeがあります。それを紹介してみましょう。


幽霊型の紹介

幽霊型とは、簡単に言えば値に出てこない型変数をつくるという型のデバインパターンです。たとえば、次のようなものです。

type Example phantom = Example

このExampleという型ですが、phantomという型引数があります。しかし、値はExampleであり、値の中にphantomという情報はありません。このphantomという型変数を、幽霊型変数と言ったりします。

このphantomに型の世界で情報を与えることによって、値をもっと限定することができます。


空でないリスト

それではどうやってつかうのでしょうか? その例を上げてみましょう。たとえば、空でないリストをコンパイル時に判定したいとしましょう。普通のList aでは、それはできません。なぜなら、List aでわかる情報はただa型のリスト型ということであり、空か空でないかの情報はありません。

ここで、幽霊型をつかいます。

type MyList isEmpty a = MyList (List a)

ここでこの、isEmptyは出てきません。このisEmptyによって、Listに空であるかないかの情報を入れていきます。ここで、isEmptyに入れるための型をつくります。

type Empty = Empty 

type NonEmpty = NonEmpty

さて、これで、リストが空であるかないかの情報を型で表わせます。たとえば、空リストは次のようになります

nil : MyList Empty a

nil = MyList []

そして、空でないリストは次のように表わすことができます。

intList : MyList NonEmpty Int

intList = MyList [1,2,3]

さて、これで、型の上では「空でないリスト」ということを主張していますが、しかしながら、このままでは、値があるMyList Empty Intをつくれてしまいます。

intList1 : MyList Empty Int

intList1 = MyList [1,2,3]

なので、moduleにして、MyListコンストラクタはMyListをopaque typeとして公開しないようにしましょう。

-- MyList(..)は公開しない。

module MyList exposing (MyList, nil, cons)

かわりに、MyListのコンストラクタとして、nilやconsを用意しておきましょう。

nil : MyList Empty a

nil = MyList []

cons : a -> MyList isEmpty a -> MyList NonEmpty a
cons a b =
case b of
MyList list -> MyList (a :: list)

これによって、空であるか、空でないかを保証することができます。どうやって、MyList Empty IntやMyList NonEmpty Intをつくるのでしょうか? MyList Empty Intは簡単です、nilから作れます。一方、MyList NonEmpty Intは、consを使います。このnilは必ず空を返すのと、consが必ず空でないリストを返すので、そういったことが保証できるのですね。

nilInt : MyList Empty Int

nilInt = nil

intList2 : MyList NonEmpty Int
intList2 = cons 42 nil

もうひとつ関数をつくってみましょう。head関数です。head関数はリストの先頭要素を取得しますが、それは前提としてリストが空でないときです。なので、head関数は空でないリストが欲しいわけですが、それを保証することができます。

head : MyList NonEmpty a -> Maybe a

head a =
case a of
MyList list -> List.head list

ここで、Maybe aを返してしまっていますが、Justしか返さないはずです。なので、Maybe aではなく、aを返したいわけですが、内部に、elmのListの型を用いているので、Maybe aにしています。


まとめ

最後にまとめましょう。


  • 幽霊型とは、簡単に言えば値に出てこない型変数をつくるという型のデバインパターン

とりあえずそれだけ覚えておけばいいかと。最後にですが、幽霊型ですが、elmではあまり一般的なものではないのですが、しかしながら、GUIの状態の管理のために有効に利用できることもあり、覚えておいて損はないかなと思います。