型を読む

このガイドの言語の基礎の節では、言語の雰囲気を掴むために多くのインタラクティブな例をざっと見てきました。さて、もう一度例を見ていこうと思いますが、今度は新たな問いを念頭に置いてやっていきましょう。つまり、この値の はなんだろうか?ということです。

プリミティブとリスト

いくつかのシンプルな式を入力して、何が起こるかを見てみましょう:

[ { "input": "\"hello\"", "value": "\u001b[93m\"hello\"\u001b[0m", "type_": "String" }, { "input": "not True", "value": "\u001b[96mFalse\u001b[0m", "type_": "Bool" }, { "input": "round 3.1415", "value": "\u001b[95m3\u001b[0m", "type_": "Int" } ]

この黒い部分 ⬆️ をクリックするとカーソルが点滅し始めます。3.1415と入力してエンターキーを押してください。すると、型であるFloatが付加されて3.1415が出力されるはずです。

さて、ここでは正確には一体何が起こっているのでしょうか?それぞれには入力値に加えてその値がどのような の値になったかが表示されています。これらは次のように読み上げることができます:

  • "hello"String型です。
  • FalseBool型です。
  • 3Int型です。
  • 3.1415Float型です。

Elmはあなたが入力したどんな値の型も推論することができます!今度はリストの動作を確認してみましょう:

[ { "input": "[ \"Alice\", \"Bob\" ]", "value": "[\u001b[93m\"Alice\"\u001b[0m,\u001b[93m\"Bob\"\u001b[0m]", "type_": "List String" }, { "input": "[ 1.0, 8.6, 42.1 ]", "value": "[\u001b[95m1.0\u001b[0m,\u001b[95m8.6\u001b[0m,\u001b[95m42.1\u001b[0m]", "type_": "List Float" } ]

これらの型は次のように読みます:

  1. String型の値の要素を持つList
  2. Float型の値の要素を持つList

は私たちが注目している値の大まかな説明になります。

関数

関数の型を見てみましょう:

[ { "input": "String.length", "value": "\u001b[36m<function>\u001b[0m", "type_": "String -> Int" } ]

roundsqrtを入力して他の関数の型を見てみましょう ⬆️

String.length関数はString -> Intという型を持っています。これは、必ずString型の引数を1つ受け取り、絶対にInt型の値を返すことを意味しています。さぁ、実際に引数を与えてみましょう:

[ { "input": "String.length \"Supercalifragilisticexpialidocious\"", "value": "\u001b[95m34\u001b[0m", "type_": "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!

型注釈を書くのは必須ではありませんが、絶対にお勧めします。利点は次のとおりです:

  1. エラーメッセージの質 — 型注釈を書いておけば、あなたがそのコードで何をしようとしているかを型注釈がコンパイラに教えてくれます。あなたの実装は間違っているかもしれません。そして今コンパイラはあなたが型注釈で記述した意図と実装を比較してくれます。コンパイラ「あなたは引数powerLevelIntだと言いましたが、Stringとして使われるようになっています!」
  2. ドキュメントとしての効用 — あとでコードを見直すとき(または同僚が初めて読むとき)、実装を非常に注意深く読む必要なくその関数に何が入って何が出ていくかを正確に理解するのに型注釈は本当に役に立ちます。

型注釈で間違いを犯す可能性がありますが、実装した内容と一致しない型を書いたらどうなるでしょうか?コンパイラはその実装内で使われている全ての型を推論し、型注釈が実際の型と一致するかどうかをチェックします。つまり、コンパイラは追加された型注釈が全て正しいことを常に確認しています。それにより、わかりやすいエラーメッセージを出すだけではなく、型が実装の内容を示す ドキュメントとしても 常に最新になっていることを担保できます!

型変数(タイプバリアブル)

より多くのElmのコードに目を通すようになると、小文字の型注釈を目にし始めるでしょう。よくある例としてList.length関数が挙げられます:

[ { "input": "List.length", "value": "\u001b[36m<function>\u001b[0m", "type_": "List a -> Int" } ]

型の中に小文字aがあることに気づきましたか?これは 型変数 と呼ばれるものです。このaが実際にどんな型になるかは、List.lengthがどのように使われるかによって変わります。

[ { "input": "List.length [1,1,2,3,5,8]", "value": "\u001b[95m6\u001b[0m", "type_": "Int" }, { "input": "List.length [ \"a\", \"b\", \"c\" ]", "value": "\u001b[95m3\u001b[0m", "type_": "Int" }, { "input": "List.length [ True, False ]", "value": "\u001b[95m2\u001b[0m", "type_": "Int" } ]

単に長さが欲しいだけなのでリストの中に何が入っているかは気にしません。つまり型変数aはどんな型にも使えるということです。もう1つよくある例を見てみましょう:

[ { "input": "List.reverse", "value": "\u001b[36m<function>\u001b[0m", "type_": "List a -> List a" }, { "input": "List.reverse [ \"a\", \"b\", \"c\" ]", "value": "[\u001b[93m\"c\"\u001b[0m,\u001b[93m\"b\"\u001b[0m,\u001b[93m\"a\"\u001b[0m]", "type_": "List String" }, { "input": "List.reverse [ True, False ]", "value": "[\u001b[96mFalse\u001b[0m,\u001b[96mTrue\u001b[0m]", "type_": "List Bool" } ]

繰り返しになりますが、型変数aList.reverseがどう使われるかによって変化します。この場合はList.reverseの型をみるとaは引数と結果にあることがわかります。これはつまりList Intを渡せば必ず同じ型List Intが返ってくるということです。一度aが何の型かを決めたらどこであってもそのaはその型になります。

Note: 型変数は小文字から始めなければなりませんが、完全な単語でも構いません。つまり例のように1文字の変数でなくても問題ありません。List.lengthの型をList value -> Intとも書けますし、List.reverseの型はList element -> List elementとも書けます。小文字で始まっていれば大丈夫です。型変数のabといった1文字のものは慣例によりいたるところで使われていますが、より具体的な名前を付けたほうがいい場合もあります。

制約付き型変数

Elmには 制約付き 型変数と呼ばれる特殊な型変数があります。最もよく使われる例はnumber型です。negate関数はnumberを使用します:

[ { "input": "negate", "value": "\u001b[36m<function>\u001b[0m", "type_": "number -> number" } ]

negate 3.1415negate (round 3.1415)negate "hi"などの式を試してみてください ⬆️

通常、型変数にはどんな型でも当てはめることができますが、numberにはIntFloatしか当てはめられません。制約は型変数の可能性を制限します。

制約付き型変数の完全なリストは次の通りです:

  • numberにはIntFloatを当てはめられます
  • appendableにはStringList aを当てはめられます
  • comparableにはIntFloat,Char,String,そしてcomparableな値で構成されるリストまたはタプルを当てはめられます
  • compappendにはStringList comparableを当てはめられます

これらの制約付き型変数は、(+)(<)のような演算子をより柔軟に使えるようにするために存在しています。

ここまでで値と関数の型については非常によく網羅してきましたが、より複雑なデータ構造が必要になり始めたら型はどのようになるでしょうか?

results matching ""

    No results matching ""