Elm

Neverについて

この記事は、わりとなんか質問があったので、それを軽くまとめます。

elmのコードを書いていると、Html.programなんていう関数があって、その型は

program : { init : (model, Cmd msg), 
            update : msg -> model -> (model, Cmd msg), 
            subscriptions : model -> Sub msg, 
            view : model -> Html msg } -> 
          Program Never model msg 

となっておりまして、最後のNeverとは一体なんぞや?みたいな質問が多いので、それを軽くまとめようと思います。

Neverとは?

Neverとは一言で言ってしまえば、値が無い型のことです。そんなことを言うと、Maybe aのNothingとかとごっちゃになるという人がいるかもしれませんが、NothingはあくまでMaybe aの値であって、NeverはNeverという型に対応する値がないということです。

elm packageのBasic につぎのように書いてます。

type Never

A value that can never happen! For context:

The boolean type Bool has two values: True and False
The unit type () has one value: ()
The never type Never has no values!

つまりですね、値がないってことはたとえば、Boolだと、2つの値、TrueとFalseがあって、Unit Typeは()という一つの値がありまして、Neverは値はありません!!!!!

ということですね。

さてさてNeverの型はどのように作られているのでしょうか? ということで、Neverの定義を見てみましょう。Basicの実装をとってくると

type Never = JustOneMore Never

ほむほむ。では、JustOneMoreという、コンストラクタがあるんで、Neverの型がつくれるんじゃ?とか思いますが、

tryToCreateNever : Never 
tryToCreateNever = 
 JustOneMore (JustOneMore (JustOneMore ... ))

となり、Neverが再帰的定義になっており、ずーっと型が再帰して値が定義できません。また、Never自体は、opaque typeなので、そもそもこんな定義じゃなくても、neverの値を作る手段は我々にはありません。

Neverのどこが良いのよさ?

とここまで書くと、Neverのについて有効性が気になりますよね。だって、Neverの型の値がないんだもの。どうつかうのよさ?ってなりますよね?

というかそもそも、値がつくれないんだから、どうやってコードに書くのさ?って疑問があるかもしれません。Neverはそもそも、Never単体で記述することはありません。型引数として用います。

たとえば、Maybe a型を考えてみましょう。絶対にNothingを返す、関数を書いてみます。

definitelyNothing : a -> Maybe Int 
definitelyNothing _ = Nothing

さて、この関数は必ずNothingを返します。その返り値の型をMaybe Intとしました。しかし、Maybe Intとする理由はとくにはありません。ただ、Nothingを返すためだけの目的として、Intをつかっただけです。

このときにNeverをつかいます。

definitelyNothing : a -> Maybe Never
definitelyNothing _ = Nothing

さて、上記の2つのコードはどう違うのでしょうか? 違いは、たとえばJustを許容するかどうかの違いにあります。つまりMaybe Intだと、Just 10などを許容してしまうかもしれませんが、Maybe Neverだと、Justの値は取れません。つまり、それだけ、Never型によって関数が制限されています。

もうひとつ例を上げましょう、つぎにResult型を考えてください。

type Result error value = Ok value | Err error

Result型は、成功(Ok)や失敗(Err)を表わします。それが考えられるものを考えます。しかしながら、時には、絶対に成功するけど、Resultで表わしたいときがあります。この場合、Result error valueのerrorの型に値なんて考えられません。このときにNeverを使えます。たとえば、aを取って、絶対にOkを返したいのであれば、次のようにします。

definitelyOk : a -> Result Never a 
definitelyOk a = Ok a

また、たとえば次のような関数があったとしましょう。

exampleFunc : (x -> b) -> (a -> b) -> (Result x a) -> b

とりあえず、実装は考えてませんが、(x -> b)と(a -> b)、Result x aを取り、bを返す関数です。もし、ここで、Resultの型が絶対にOkだけを返すと考えるとすると,

exampleFunc : (a -> b) -> (Result Never a) -> b

と定義できるかもしれません。ここで、気づいて欲しいのは,xという型引数がなくなり、関数としてシンプルになったのではないでしょうか?

最後に冒頭に,紹介したHtml programですが、まずprogramWithFlagsの関数があり、

programWithFlags : { init : flags -> (model, Cmd msg), 
                     update : msg -> model -> (model, Cmd msg), 
                     subscriptions : model -> Sub msg, 
                     view : model -> Html msg } -> 
                   Program flags model msg

このflagsの型引数がなくなったバージョンがHtml programです。

program : { init : (model, Cmd msg), 
            update : msg -> model -> (model, Cmd msg), 
            subscriptions : model -> Sub msg, 
            view : model -> Html msg } -> 
          Program Never model msg 

上記を見比べていると、Programの型にはflagという型引数があるのですが、programの関数の戻り値の型はflagを持たないProgramのであることを表わしており、programWithFlagと比較すると、flagを持つ持たないとで、引数のinitの型が異なることがわかると思います。つまり、flagを持つか持たないかを、Neverを用いることによって型で表現して、関数の型を厳格にしているんですね。

flagに関して詳しくは、https://guide.elm-lang.org/interop/javascript.html を参照してください。

まとめ

最後に簡単にまとめましょう。

  • Neverは、値の無い型である。
  • Neverを型引数に使うことによって、型が厳格になったり、シンプルになったりする。

以上です。