Edited at
ZeneloDay 2

親切過ぎるエラーメッセージを届ける言語 Elm

謎の集団zeneloでうごめく ドラクエ + Elm 星人 ABです。今年のアドベントカレンダーは基本Elmとテストについて突っ走ろうと思っています。無事3枚目に到達したElm Advent Calendarもよろしくおねがいします。

例によって、この記事もElmについての解説記事になりますがElm自体の説明は公式ページや無数にあるElmの解説記事に任せることにします。(例: ヤギさんの記事)


バージョンアップで強化された親切エラーメッセージ

今年の8/21にめでたくバージョン0.19を迎えたElmですが以下のような特徴がありました。


  • コンパイルがめちゃくちゃ高速化された

  • 出力されるJavaScriptのファイルサイズがめちゃくちゃ減った

  • シングルバイナリになった

  • また演算子(機能)が減った

  • シャドーイングが禁止された

  • エラーメッセージがド親切になった

  • その他諸々

詳しくは、ジンジャーさんのElm 0.19の主な変更点を御覧ください。

今回は元々親切だったエラーメッセージが0.19になり、さらに親切心が増して地母神のようになったので紹介したいと思います。

(ここで神官ちゃんの画像)


エラーの紹介の仕方

Elmはフロントエンド用の静的型付け言語のため、一番親しいTypeScriptと同じようなコードを並べて紹介します。私がものぐさなため、ElmはREPLを使用し、TypeScriptはPlayground - TypeScriptのオプション(strictNullChecks等)をありったけ使用はしています。しかし、それよりもっとlinterを詳しく設定すれば、もっと親切なエラーが出るよ・・・等の情報があればコメント欄にてフォローお願いいたします。しかし、一つの主張としてlinterのようなコンパイラ以外のツール等を利用せずとも、親切に怒ってくれるママのようなElmというところが愛すべきポイントかなと思っています。それでは、親切過ぎるエラーを紹介していきます。


ド親切エラー集


存在しない演算子を使おうとした編

まずは他の言語から移ってきた、もしくは同時に複数の言語を扱うことは今はあまり珍しいことではありません。時には、別の言語で使っていた便利な演算子を使おうとしてサポートされていなかったなんてケースが稀にあると思います。

TypeScriptはシンタックスに間違いがあるよ。とクールに教えてくれます。

> const x = 12 # 2;

const x = 12 # 2;
^

SyntaxError: Invalid or unexpected token

Elmは・・・すごいです。文法エラーを指摘した上で、

中置記法で書けるものとしては(+)もしくは、(==)があるよ。それじゃないかしら?

なんて提案をしてくれます。

> 12 $ 2

-- PARSE ERROR ------------------------------------------------------------- elm

Something went wrong while parsing repl_value_1's definition.

4| repl_value_1 =
5| 12 $ 2
^
I was expecting:

- an argument, like `name` or `total`
- an infix operator, like (+) or (==)
- the rest of repl_value_1's definition. Maybe you forgot some code? Or maybe
the body of `repl_value_1` needs to be indented?

また冒頭で説明したとおり0.19でのアップデートで削られた演算子があり、それをうっかり使ってしまったケースの場合です。もしJavaScriptで言う(%)の演算子を使いたいのであれば、remainderByもしくは、modByを参照してみると良いかも、とリンクを載せた上で、負の数のときに扱いが変わるわよ。何ていうさらに嬉しいアドバイスを与えてくれます。ただ原因を述べるだけでなく、アフターフォローが手厚いです。

> 12 % 2

-- UNKNOWN OPERATOR -------------------------------------------------------- elm

Elm does not use (%) as the remainder operator:
5| 12 % 2
^
If you want the behavior of (%) like in JavaScript, switch to:
<https://package.elm-lang.org/packages/elm/core/latest/Basics#remainderBy>

If you want modular arithmetic like in math, switch to:
<https://package.elm-lang.org/packages/elm/core/latest/Basics#modBy>

The difference is how things work when negative numbers are involved.


else節を忘れた編

if-else文(式)を書いたときに、elseをうっかり忘れてしまったなんていう場合です。

TypeScriptはreturnが掛けていることを指摘してくれて、型を合わせるならundefinedが含まれていないといけないね。と教えてくれます。

function foo(x: number): string {

if (x == 2) {
return "two";
}
}

// Function lacks ending return statement and return type does not include 'undefined'.

一方Elmでは、そもそもif-elseはセットのためPARSE ERRORとして指摘が入ります。曖昧さを許さず実行時エラーを起こさないようにするためですね。ただ文法エラーです。とだけでは終わりません。エラーが起きた原因をElmママは考えてくれます。


  • else節。ifを書いたら両方扱わないといけないのですよ。

  • ifで終わっちゃってるわよ?コード書ききるの忘れちゃったかしら。(REPLではなく実際コードを書く場合)インデントを入れる必要があるわよ。気をつけなさい。

うーん。文法エラー一つでとっても親切ですね・・・。ママの愛を感じます・・・。

> foo x = if x == 2 then "two"

-- PARSE ERROR ------------------------------------------------------------- elm

Something went wrong while parsing an `if` expression in foo's definition.

4| foo x = if x == 2 then "two"
5|
^
I was expecting:

- an `else` branch. An `if` must handle both possibilities.
- an argument, like `name` or `total`
- the end of that `if`. Maybe you forgot some code? Or maybe the body of `foo`
needs to be indented?


String literal type/Custom type 編

ifと似たようなケースとして、String literal typeを使うケースです。switch-caseのパターンを網羅するとエラーが出なくなると言うのが、String literal typeの特徴となります。残念ながらパターンが網羅されていないときは、ifと同じエラーしか吐いてくれませんでした。

type Foo = "bar" | "hoge";

function foo2num(foo: Foo): number {
switch (foo) {
case "bar":
return 1;
}
}
// Function lacks ending return statement and return type does not include 'undefined'.

Elmには、String literal typeは存在しませんが似たような性質を持つものとして、Custom Typeが存在します。Custom Typeは単なる分岐を行うだけではなく、任意の型の値を持つことができるのが特徴となります。

ママはCustom Typeの分岐を行うcase of においてパターンが足らない場合には、caseの分岐はすべて満たさないといけないわよ。と優しく教えてくれます。しかもそれだけでは終わりません。以下の例では、忘れていた分岐であるHogeが忘れているわよ。と教えてくれます。また、なぜ分岐が無ければならないかの解説も入れてくれます。もし実行時に分岐がなかったらアプリがクラッシュしてしまう!もしれそれを意図的に起こしたいのであればDebug.todo関数を使いなさい。とネクストアクションの示唆までしてくれます。補足として、分岐が足らないパターンの参考リンクも置いて言ってくれます。なんて・・・なんて・・・親切なのでしょう・・・。

> type Foo = Bar | Hoge

> foo2num foo = \
| case foo of\
| Bar -> 1
-- MISSING PATTERNS -------------------------------------------------------- elm

This `case` does not have branches for all possibilities:

6|> case foo of
7|> Bar -> 1

Missing possibilities include:

Hoge

I would have to crash if I saw one of those. Add branches for them!

Hint: If you want to write the code for each branch later, use `Debug.todo` as a
placeholder. Read <https://elm-lang.org/0.19.0/missing-patterns> for more
guidance on this workflow.


存在しないプロパティを参照しようとした編

最後は、オブジェクトに存在しないプロパティを参照しようとしてしまった。何ていうケースです。

TypeScriptは、{ a: number; }という型には、bなんてプロパティは存在しないよ。と言ってくれます。

[{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }].map((obj) => obj.b);

Property 'b' does not exist on type '{ a: number; }'.
any

Elmはどこに問題があるかを下線でしっかり伝えた上で(今更ですが、エラーのほとんどが下線でしっかり教えてくれます。)、現状の型が何で、自分が処理しようとしている型がどんな型になるかを明確に教えてくれます。その上でヒントとして、bはタイポしたんじゃないの?本当はaって打とうとしたんじゃないの? 型定義をしっかり書きなさい。そうしたら、もっと親切に教えることができるわよ!と親切に追い打ちを掛けてくれます・・・。ママ・・・ママァアアアアアアアア。

> [{ a = 1 }, { a = 2 }, { a = 3 }, { a = 4 }] |> List.map .b

-- TYPE MISMATCH ----------------------------------------------------------- elm

This function cannot handle the argument sent through the (|>) pipe:

4| [{ a = 1 }, { a = 2 }, { a = 3 }, { a = 4 }] |> List.map .b
^^^^^^^^^^^
The argument is:

List { a : number }

But (|>) is piping it a function that expects:

List { a | a : number, b : b }

Hint: Seems like a record field typo. Maybe b should be a?

Hint: Can more type annotations be added? Type annotations always help me give
more specific messages, and I think they could help a lot in this case!


まとめ

Elmのエラーの特徴として以下のことが挙げられます。エラーがここまで丁寧だと学習で挫折することが非常に難しいという事を実感していただけたでしょうか。プログラミング初心者にこそオススメしたい言語になっております。


  • 何のエラーが起きたか

  • エラーが起きた理由

  • エラーを解消するためのヒント

  • 補足資料へのリンク

ここまで徹底して親切なエラーを作り出せる理由として 実は、この記事だけでElmの構文のほとんどを紹介してしまっています。余分な構文はどんどん削られていく言語です。そのため、エラーのパターンが予測しやすく手厚いサポートができるようになっています(それを差し置いてもかなり親切過ぎますが・・・)。TypeScriptでは機能がドンドン豊富になっていって嬉しいことが多い反面、エラーの出現パターンが複雑で事細かなエラーを出すには、linterなどのツールに頼らざるを得ないケースがほとんどです。

また、TypeScriptでは人の意志が弱いがためにanyと書いてしまったことが少なからずあるのではないでしょうか。自分自身が気をつけていたとしても使用ライブラリやチームの開発メンバーがそれを徹底していなければ、バグの温床となってしまいます。Elmでは型に対して厳密なためヒューマンエラーが一切起こらないようになっています。是非、安全なWEBアプリケーションを構築したいと思っている場合には、ご検討ください。