型を読む
このガイドの言語の基礎の節では、言語の雰囲気を掴むために多くのインタラクティブな例をざっと見てきました。さて、もう一度例を見ていこうと思いますが、今度は新たな問いを念頭に置いてやっていきましょう。つまり、この値の 型 はなんだろうか?ということです。
プリミティブとリスト
いくつかのシンプルな式を入力して、何が起こるかを見てみましょう:
この黒い部分 ⬆️ をクリックするとカーソルが点滅し始めます。3.1415
と入力してエンターキーを押してください。すると、型であるFloat
が付加されて3.1415
が出力されるはずです。
さて、ここでは正確には一体何が起こっているのでしょうか?それぞれには入力値に加えてその値がどのような 型 の値になったかが表示されています。これらは次のように読み上げることができます:
- 値
"hello"
はString
型です。 - 値
False
はBool
型です。 - 値
3
はInt
型です。 - 値
3.1415
はFloat
型です。
Elmはあなたが入力したどんな値の型も推論することができます!今度はリストの動作を確認してみましょう:
これらの型は次のように読みます:
String
型の値の要素を持つList
型Float
型の値の要素を持つList
型
型 は私たちが注目している値の大まかな説明になります。
関数
関数の型を見てみましょう:
round
やsqrt
を入力して他の関数の型を見てみましょう ⬆️
String.length
関数はString -> Int
という型を持っています。これは、必ずString
型の引数を1つ受け取り、絶対にInt
型の値を返すことを意味しています。さぁ、実際に引数を与えてみましょう:
まずString -> Int
の関数にString
を与えてみましょう。結果はInt
です。
String
以外を与えたら何が起こるでしょうか?String.length [1,2,3]
と入力するかString.length True
と入力して動作を見てみましょう ⬆️
String -> Int
の関数はString
型の値を引数にしなくてはならないことが理解できたでしょう!
Note: 複数の引数を取る関数は、より多くの矢印を持つことになります。例えば、2つの引数をとる関数はこうなります:
[ { "input": "String.repeat", "value": "\u001b[36m<function>\u001b[0m", "type_": "Int -> String -> String" } ]
String.repeat 3 "ha"
、このように2つ引数を与えると"hahaha"
が生成されます。->
を引数の区切り文字として考えるのは奇妙に思えますが、本当の理由はここで説明しています。それはとてもすっきりした説明です!
型注釈(タイプアノテーション)
今のところElmに型を推論させているだけですが、定義の上の行に型注釈を書くこともできます。つまり、次のようにコードを書くことができます:
half : Float -> Float
half n =
n / 2
-- half 256 == 128
-- half "3" -- error!
hypotenuse : Float -> Float -> Float
hypotenuse a b =
sqrt (a^2 + b^2)
-- hypotenuse 3 4 == 5
-- hypotenuse 5 12 == 13
checkPower : Int -> String
checkPower powerLevel =
if powerLevel > 9000 then "It's over 9000!!!" else "Meh"
-- checkPower 9001 == "It's over 9000!!!"
-- checkPower True -- error!
型注釈を書くのは必須ではありませんが、絶対にお勧めします。利点は次のとおりです:
- エラーメッセージの質 — 型注釈を書いておけば、あなたがそのコードで何をしようとしているかを型注釈がコンパイラに教えてくれます。あなたの実装は間違っているかもしれません。そして今コンパイラはあなたが型注釈で記述した意図と実装を比較してくれます。コンパイラ「あなたは引数
powerLevel
がInt
だと言いましたが、String
として使われるようになっています!」 - ドキュメントとしての効用 — あとでコードを見直すとき(または同僚が初めて読むとき)、実装を非常に注意深く読む必要なくその関数に何が入って何が出ていくかを正確に理解するのに型注釈は本当に役に立ちます。
型注釈で間違いを犯す可能性がありますが、実装した内容と一致しない型を書いたらどうなるでしょうか?コンパイラはその実装内で使われている全ての型を推論し、型注釈が実際の型と一致するかどうかをチェックします。つまり、コンパイラは追加された型注釈が全て正しいことを常に確認しています。それにより、わかりやすいエラーメッセージを出すだけではなく、型が実装の内容を示す ドキュメントとしても 常に最新になっていることを担保できます!
型変数(タイプバリアブル)
より多くのElmのコードに目を通すようになると、小文字の型注釈を目にし始めるでしょう。よくある例としてList.length
関数が挙げられます:
型の中に小文字a
があることに気づきましたか?これは 型変数 と呼ばれるものです。このa
が実際にどんな型になるかは、List.length
がどのように使われるかによって変わります。
単に長さが欲しいだけなのでリストの中に何が入っているかは気にしません。つまり型変数a
はどんな型にも使えるということです。もう1つよくある例を見てみましょう:
繰り返しになりますが、型変数a
はList.reverse
がどう使われるかによって変化します。この場合はList.reverse
の型をみるとa
は引数と結果にあることがわかります。これはつまりList Int
を渡せば必ず同じ型List Int
が返ってくるということです。一度a
が何の型かを決めたらどこであってもそのa
はその型になります。
Note: 型変数は小文字から始めなければなりませんが、完全な単語でも構いません。つまり例のように1文字の変数でなくても問題ありません。
List.length
の型をList value -> Int
とも書けますし、List.reverse
の型はList element -> List element
とも書けます。小文字で始まっていれば大丈夫です。型変数のa
やb
といった1文字のものは慣例によりいたるところで使われていますが、より具体的な名前を付けたほうがいい場合もあります。
制約付き型変数
Elmには 制約付き 型変数と呼ばれる特殊な型変数があります。最もよく使われる例はnumber
型です。negate
関数はnumber
を使用します:
negate 3.1415
、negate (round 3.1415)
、negate "hi"
などの式を試してみてください ⬆️
通常、型変数にはどんな型でも当てはめることができますが、number
にはInt
かFloat
しか当てはめられません。制約は型変数の可能性を制限します。
制約付き型変数の完全なリストは次の通りです:
number
にはInt
かFloat
を当てはめられますappendable
にはString
かList a
を当てはめられますcomparable
にはInt
かFloat
,Char
,String
,そしてcomparable
な値で構成されるリストまたはタプルを当てはめられますcompappend
にはString
かList comparable
を当てはめられます
これらの制約付き型変数は、(+)
や(<)
のような演算子をより柔軟に使えるようにするために存在しています。
ここまでで値と関数の型については非常によく網羅してきましたが、より複雑なデータ構造が必要になり始めたら型はどのようになるでしょうか?