Elmでも使えるPhantomTypeについて

[Fringe81 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/fringe81)の20日目です。

# PhantomType とは

定義は単純で、「値に使用しない型引数を持つ型」のことをPhantomTypeを言います。

Elmで書くと以下のようになります。

```elm
type Phantom a = Phantom Int
```

Scalaではこう書けます。

```scala
case class Phantom[A](value: Int)
```

PhantomTypeはこのように型の情報を増やしてコンパイル時により多くのエラーを見つける目的で使用されます。

PhantomTypeという名前の由来は、「付与した型の情報がコンパイル後には見えなくなってしまうのが幽霊っぽいから」とか、「型には存在するのに値には存在しない、あるようなないような不思議な感じが幽霊っぽいから」など諸説あるようです。個人的には後者推しです。

また定義の方法が簡単なので、特に頑張って工夫をしなくても型変数やジェネリクスが定義できる言語であれば使用することができるという特徴があります。

本記事ではElmで定義したPhantomTypeについて解説しています。内容によっては他の言語には適用できないものもありますのでご了承ください。あくまで「Elmで定義したPhantomType」として読んでいただければと思います。

# PhantomTypeの使用例

アプリケーションでユーザーに文字列を入力してもらい、送信ボタンを押したらその文字列をサーバーに送信するという、よくある処理を書くシチュエーションを考えます。

この時、入力された文字列をそのままサーバーに送信するのではなく、サニタイズしてから送信したい場合、普通はこんな雰囲気のコードを書くと思います。

```elm
let
    sanitized =
        sanitize inputString
in
sendToServer sanitized
```

`sendToServer`関数の前に入力値の`inputString`を`sanitize`関数に通しておく。普通ですね。

しかしこのプログラムには欠点があります。`sanitize`関数を呼ばずに、`sendToServer`関数にそのまま`inputString`を渡してもコンパイルが通ってしまうのです。`sendToServer`関数の前には必ず`sanitize`関数を呼んでもらいたい...。こんなとき、PhantomTypeの出番です。

まず初めに、文字列を値に持つPhantomTypeを定義します。

```elm
type InputString a = InputString String
```

次に、型変数`a`に当てはめるための型を定義します。PhantomTypeを使う際にはここの型を変えることによって「違う意味を持つ型である」ということを表現します。今回区別したいのは生の入力値とサニタイズ済みの文字列なので次の二つを定義します。

```elm
type Raw
type Sanitized
```

これによって、生の入力値を表す型`InputString Raw`とサニタイズ済みの文字列を表す型`InputString Sanitized`を区別することができました。

最後に、文字列を扱っていた関数の引数や戻り値で`InputString a`を使うように変更します。

```elm
sanitize : InputString Raw -> InputString Sanitized
sanitize raw = ...

sendToServer : InputString Sanitized -> HttpRequest
sendToServer sanitized = ...
```

以上です！これで`inputString`をそのまま`sendToServer`に渡したときにはコンパイルエラーを出してくれるようになりました。ありがとうコンパイラ。

ただ、これで確かにPhantomTypeを使うことはできましたが、完全に理解したと言うにはまだ早いです。勘の良い方ならお察しかもしれませんが、「生の入力値」と「サニタイズ済みの文字列」を区別するだけであればPhantomTypeを使わなくてもできるのです。

次は似た処理をPhantomTypeを使わずに行った時との比較をして、PhantomTypeを使った時の特徴を見てみたいと思います。

## UnionTypeを使ってみる

次のようなUnionType[^1]を定義します。

[^1]: Elm 0.19でCustomTypeという呼び方になりました。

```elm
type InputString
    = Raw String
    | Sanitized String
```

これは`InputString`という型を`Raw String`、`Sanitized String`という二つの値を持つものとして定義しています。これでも確かに生の入力値とサニタイズ済みの文字列を区別できますが、PhantomTypeを使った場合と大きく違うのは、値で区別しているという点です。

値で区別しているので、生の入力値を`sendToServer`関数に渡した時のエラーは実行時に起きることになります。それに加えて、`InputString`型を扱う関数は必ず生の入力値とサニタイズ済みの文字列の両方を考慮した処理を書かなければなりません。さらに、生の入力値だった場合は`sendToServer`関数は処理を継続できず、エラー値を返却する必要があるので、戻り値を正常値とエラー値の両方を表現できる型にする必要があります。

```elm
sendToServer : InputString -> Result ErrorMessage HttpRequest
sendToServer input =
    case input of
        Raw _ ->
            Err "サニタイズしてないよ"

        Sanitized sanitized ->
            ...
```


そしてさらに`sendToServer`関数の戻り値をパターンマッチして...と、どんどんプログラムが煩雑になっていきます。UnionTypeでも似たようなことはできますが、PhantomTypeを使った時のように簡潔なコードにはならず、エラーも実行時まで遅延されてしまうようです。

## 単に別の型として定義してみる

エラーをコンパイル時に見つけるために、今度は二つの文字列を全く別の型として定義してみます。

```elm
type RawString = RawString String
type SanitizedString = SanitizedString String
```

続けて、これに合わせて`sanitized`関数と`sendToServer`関数を定義してみます。

```elm
sanitize : RawString -> SanitizedString
sanitize raw = ...

sendToServer : SanitizedString -> HttpRequest
sendToServer sanitized = ...
```

こうすれば`sendToServer`関数は`SanitizedString`型しか引数に取らないので、この方法でもコンパイル時にエラーを見つけることができそうです！この時点では確かにPhantomTypeの代わりができています。

ただ、この方法とPhantomTypeには違いがあります。それは抽象化できるかどうかです。

例えば、生の入力値とサニタイズ済みの文字列は両方とも`String`型の値を含んでいるので、`String`型を適用できる関数に値を渡したい場面があると思います。そのために、`RawString`型と`SanitizedString`型から中身の`String`を取り出す関数を作ってみます。

二つを別の型として定義する方法では、それぞれの型向けに別の関数を作る必要があります。

```elm
rawStringValue : RawString -> String
rawStringValue (RawString value) = value

sanitizedValue : SanitizedString -> String
sanitizedValue (SanitizedString value) = value
```

一方PhantomTypeを使った場合は、型変数を決めない`InputString a`型の関数として定義すれば、生の入力値もサニタイズ済みの文字列も同じ型の値として扱うことができるので、作る関数は一つだけで済みます。

```elm
inputStringValue : InputString a -> String
inputStringValue (InputString value) = value
```

今回は区別する対象が二つなので少し面倒なだけで済みますが、その数が増えると明らかに冗長になります。また、関数合成をするときにも二つの値の違いを意識しながら書かないといけなくなり、プログラムが複雑になる原因になります。「区別はしたいけど、ある時には同じものとして扱いたい」という場合に、PhantomTypeは向いていると言えます。

# まとめ

PhantomTypeの定義自体が広いのでその使い道をズバリとは言えませんが、使う目的はある程度決まっています。

- 似た値によるエラーをコンパイル時により多く検知すること
- その目的を簡潔なプログラムで達成すること

本記事ではElmでのPhantomTypeについて考えてきましたが、みなさんが普段書いている言語でもPhantomTypeが使えそうであればその使い道について考えてみてはいかがでしょうか！