Elmは構文が少なくプログラミング初心者でもすぐにWebアプリが作れてしまうシンプルな言語です。どれほどシンプルかと言うと、以下の構文のページの分量が物語っています。
構文が短いだけではシンプルな言語かはわかりません。もう一つElmの構文においてシンプルな理由は、ほとんどが式であるということです。
文と式
プログラミング言語において式と対になり多くの人に馴染みがあるのが文でしょう。Wikipediaから説明を抜粋してみます。
プログラムにおける文(ぶん、statement)とは、コンピュータプログラミング言語によるプログラムを構成するもののひとつで、一般に手続きを表すものである。
類似する言葉として式がある。式は、必ずしも手続きを表さず、文とは異なり値を持つ
手続きをもう少し噛み砕くと、コンピュータへの命令です。コンピュータへの命令はプログラミング言語ごとに異なるので、その言語のある命令がどういった意味を表すかを押さえる必要があります。
式はとてもシンプルです。評価(計算)をすると値を返すものが式です。例えば以下のようなものが式となります。
1 + 1 // = 2
Elmのいろんな式
それでは手続き型とElmの式をいろいろ比べていきましょう。
if式
手続き型言語では分岐を表す手法として、if文があります。文なので、if自体が値を返すことはありません。そのため一時変数を用意し、その都度、代入をしていく必要があり代入のし忘れによる実装ミスを誘発したりします。また、不本意な初期化をしなければならなかったり、処理が冗長になりがちです。
let result = null;
if(key == 40) {
result = n + 1;
} else if(key == 38) {
result = n - 1
}
else {
result = n;
}
Elmの特徴として、分岐を表すものが式であることが挙げられるでしょう。Elmでは実行時エラーが原則起きません。これは分岐漏れをコンパイラが許さない制約を持つからです。つまりifを書いた際にはelseを省略することができません。また式であるため、returnを省略できます。(そもそもreturnが存在しません)
if key == 40 then
n + 1
else if key == 38 then
n - 1
else
n
-- > key = 40, n = 3
-- 4
-- > key = 38, n = 5
-- 4
-- > key = 0, n = 10
-- 10
以下のようにif式(が評価した値)に対して演算をすることができます。
(if x == 2 then 10 else 5) + 5
多くの言語では三項演算子(条件演算子)を利用してif式のように書くことも出来ます。しかし、分岐が多くなったり、ネストしたり、複雑な計算を入れたい場合にはifが式であるに越したことはありません。逆にElmには三項演算子が存在しません。これも誰が書いても同じようになるように、シンプルさを保つために、そのようになっています。
case式
手続き型言語ではswitch-case文により、ある値に対する分岐を見やすくすることができます。しかしif文と同様の問題が生じます。さらには、break文を忘れずに書かなければいけないというお作法に準拠しなければなりません。
let result = null;
switch (x) {
case 1:
result = 'one';
break;
case 2:
result = 'two';
break;
case 3:
result = 'three';
break;
default:
result = 'other';
break;
}
Elmのcaseは式のため、breakも代入も必要ありません。ifと同様に演算をかけることもできます。Elmのcase式はもっとすごい機能を備えています。気になる方はこちらの記事をどうぞ。
case x of
1 ->
"one"
2 ->
"two"
3 ->
"three"
_ ->
"other"
Record Setter
オブジェクト指向言語では、オブジェクトをミュータブル(可変)のものとして扱うことが多いです。JavaScriptでは、いくらconstキーワードを使ってオブジェクトを定義していても、中のプロパティを書き換えることができてしまいます。特に外部のAPIなどを利用するときは、値の書き換えに注意が必要です。
> const foo = { bar: 1, hoge: 'abc' }
undefined
> foo.bar = 10
10
> foo
{ bar: 10, hoge: 'abc' }
ElmのRecord(に限らずあらゆるデータ構造)は、一度定義をしたら一生涯その値が書き換わることがありません。極めて安全にデータを取り扱うことができます。
> foo = { bar = 1, hoge = "abc" }
{ bar = 1, hoge = "abc" }
: { bar : number, hoge : String }
> foo2 = { foo | bar = 10 }
{ bar = 10, hoge = "abc" }
: { bar : number, hoge : String }
> foo
{ bar = 1, hoge = "abc" }
: { bar : number, hoge : String }
JavaScriptのスプレッド演算子もRecordとほぼ同様のことが行なえます。しかし、shallow copyのためネストした子供は参照が渡されてしまったり、スプレッド演算子以外のオブジェクトの書き換え方法が提供されてしまっている以上、ルールやツールによる制限が必要になってしまいます。人間が気をつける運用はなるべく避けたいところです。
Let式
手続き型言語では、状態を表す変数を容易に宣言できてしまいます。状態を書き換えるような処理は文と合わさることで、とても複雑性を増し人間が読み解くのがこんなんになってきます。
const f = (x: number) => {
let result = 10 * x;
if(x > 10) {
let tmp = x * 2;
result += tmp;
} else {
result += x;
}
return result;
}
ElmのLet式は、letによる式の宣言とinによる最終的な結果を表す一つの式からなります。letで宣言された式は、inの式からしか参照ができません。tenXやdoubleXは値が束縛されていますが、変数と違い評価された値から一生値が変わることがありません。そのため計算結果の予測が人間に優しい形になっています。また、inには一つの式(if式も一つの式です。)しか書けないため、関数が大きくなり過ぎるのを防いぐ役割も果たしています。(残念ながらLet式のネストにより巨大な関数を書くことができてしまいますが)
f : Int -> Int
f x =
let
tenX =
10 * x
doubleX =
x * 2
in
if x > 10 then
tenX + doubleX
else
tenX + x
まとめ
構文自体が少ないことに加え、ほとんどの構文が式であることはシンプルさを保つための言語機能としてとても強力です。今回の記事でほとんどの構文を紹介しきることができてしまいました。それ以外は関数で表現することができてしまいます。つまり、定義さえ見れば言語としての機能を意識する必要は無いため学習コストがとても低いです。それでは素敵なElmライフを!