はじめに
Elm
という名前を聞いたことがありますか?
Redux
を使ったことがある方は、「ああ、Reduxのもとになった言語ね!」と思ったかもしれません。
世間では、「Elmは設計思想としてはいいけど、Haskellの知識がないとわからないし難しい」みたいな嘘にだまされて、「JSの知識だけで使えるし、簡単なReduxの方を使ってみよう!」と思う方も少なくないようです。
しかし、Elmは難しくありません。
Elmは、初心者でも簡単に使える言語であり、かつ実業務で採用してもJSで書くよりも保守性や拡張性が高いコードを書くことができます。
実際に、最近のバージョン0.17, 0.18, 0.19では、機能が追加されるどころか、「この機能は紛らわしい」「この機能は別の機能で代替できる」という理由で削られ、どんどんシンプルになっていっています。
(Elmの興味深い思想についてもっと知りたい方はElmはどんな人にオススメできないかをご覧ください)
いままでJSを書いてきた方にとって、Elm は新しい機能の使い方をおぼえる必要はほとんどなく、JSの知識だけでも理解できる文法です。
また、要求されるJSのレベルも、それほど高い知識は求められません。
たとえば私は1日10時間以上寝ないと頭が働かない体質の上、会社の経営や、バックエンド側のプログラミング業務などにより、あまりフロントエンドの学習に時間を割くことができない中、片手間にJSを学習して使っていた程度のレベルです。
むしろ、そういう時間が限られた環境だからこそ、学習コストもJSに比べて低く、保守性と拡張性の高いプログラムを書けるElmを愛用しています。
この記事では、Elmの
- ためし方
- 変数の使い方
- 関数の使い方
- 配列の使い方
について、JSとの差異に触れながら紹介していきます。
この内容は最新の Elm 0.19 で正しく動作することを想定しています。
Elmのためし方
Elmは初心者が簡単に使えることを目的としていますから、
もちろん、いまから1秒後に使いはじめることができます。
オンラインの対話環境にアクセスし、ためしに以下の式を入力してみましょう。
> 3+4
(>
は入力する必要ありません)
その下に、計算結果が表示されましたね?
7 : number
とても初心者にやさしいです。
以降の内容についても、こちらのオンラインの対話環境で試しながら読み進めてください。
基本文法
Elm は初心者にもやさしいので、基本文法はとても少ないです。
なんと、Web ページ1枚で、ほぼすべての文法を説明できてしまいます。
JavaScriptの基本文法と比べて、10分の1どころじゃなく少ないことに驚くでしょう。
なんとこれだけの言語機能だけで、JSで書くよりも拡張性、保守性の高いWebアプリケーションを作成できるのです。
以降では、これらの基本文法について、
- JSのどの課題を解決するのか
- なぜJSよりも簡単なのか
について触れながら紹介していきます。
変数への意図しない代入を防ぐ
いきなりですが、問題です。
JSで、引数を2倍した値を返す関数double
と、引数に1を加えた値を返す関数succ
を定義しました。
実際に2
を引数として与えて評価したところ、どうやら返り値は正しいようです。
> double(2)
4
> succ(2)
3
しかし、以下のプログラムを実行したところ、想定とは異なる結果になってしまいました。
var x = 2
console.log(double(x));
console.log(succ(x));
console.log
に想定していた挙動は、以下の値を出力することです。
4
3
でも、実際には、以下の出力が得られてしまいます。
4
5
succ
関数に置いて、引数が2
の場合の挙動とは異なりますが、なぜでしょうか?
初心者殺しのJSの場合、いろんな可能性がありますが、1つの可能性として、double
関数を以下のように定義していることが考えられます。
function double(a) {
x = a * 2;
return x;
}
一見したところ問題ないように見えますが、x
に値を代入する際に、var
をつけ忘れています。
これにより、double
関数の定義内でのみ使われるローカル変数x
に値を代入しているのではなく、グローバル変数のx
に対して、値を上書きしてしまいます。
ゆえに、double
を呼んだことにより、グローバル変数のx
に4
がセットされ、succ(4)
の結果である5
が出力されたのでした。
本来、複雑な計算をブラックボックス化することが関数の目的なのにも関わらず、関数定義の中身をよ〜く見ないと、この挙動の理由が解析できないなんて、言語として難しすぎます。
JSの比較的あたらしい仕様であるES6では、const
の使用が可能になり、変数への再代入に起因する上記のようなエラーをある程度防げるようになりましたが、ES6は現在広く使われている仕様であるES5への後方互換性を維持する必要があったため、あくまでconst
を忘れないように気をつけてプログラムを書かないといけません。
これでは本質的にはなんら変わらず、常にプログラマが気をつけていないといけません。1
そこで、初心者にもやさしい Elm は、なんとデフォルトでconst
キーワードを使ったように振る舞います。
たとえば、以下のプログラムは、コンパイル時にエラーで指摘されます。
a = 4
a = 5
対話環境で、以下のように入力して、エラーがでることを確認してください。
> a = 4 \
| a = 5
なお、1行目の最後にある\
は、対話環境に対して、「このコードは、まだ終わっていない」と伝えるためのものです。
もちろん、実際にファイルにプログラムを書いてコンパイルする場合には、末尾の\
は不要です。
JSと比較して、機能を制限しているだけで、余計な機能を追加せずに、コードの質を保つことに貢献しています。
関数
テストしやすさ
さきほどのdouble
関数の例は、入出力に対する何千通りものテストを書いても、防げないバグでした。
このようなバグが生まれてしまうのは、JSにおいて関数の実行が、出力値以外の場所に影響を与えてしまいうることが原因です。
double
関数は、関数の出力以外の環境(具体的にはx
という名前のグローバル変数)を変更できることが問題でした。
逆に、以下のように関数の返り値が外部の環境(この例では現在時刻)に影響される関数も定義できます。
if (isLeapYear()) {
console.log('今年はうるう年です');
} else {
console.log('今年はうるう年ではありません');
}
function isLeapYear () {
return (new Date()).getFullYear() % 4 === 0
}
でも、こんな環境依存の関数、テストしにくくて仕方がありません。
おそらく、ふだんからJSを使っているあなたは、以下のようにリファクタリングしますよね?
const current = new Date();
if (isLeapYear(current)) {
console.log('今年はうるう年です');
} else {
console.log('今年はうるう年ではありません');
}
function isLeapYear (d) {
return d.getFullYear() % 4 === 0
}
これにより、isLeapYear
関数のテストにおいて、入出力のみを検査すればよくなり、飛躍的にテストがしやすくなります。
また、関数が目に見えない状態を保持しなくなり、コードの保守性も高まります。
Elm は、これらの理由から
- 出力が入力以外に依存する関数を自分で定義することはできない
- 出力以外に、関数外部の世界に干渉するすべを持たない
という戦略を採用しています。
ここでも、できないことを増やしただけで、機能としてはとくに覚えることはありません。
関数の呼び出し
Elmで好まれる関数の呼び出しの記法は、以下の形式です。
foo arg1 arg2
もちろん、JSっぽく以下のように書くこともできます。
(この呼び出し方をする場合は、関数定義の段階でこの呼び出し方用の定義をする必要があります)
foo(arg1, arg2)
でもカッコをつける/つけないと、引数の区切りが,
か半角スペースかくらいで特に本質的な違いはありませんね?
どうしても宗教上の理由でJSっぽく書きたい方は下の書き方を採用すればいいだけです。
でも上の記法をつかうことで、Elm内でHTMLを表現するときに格段に見やすくなります。
たとえば以下のコードはElmの関数を使っていますが、パッと見て構造がわかりやすく、どんなHTMLコードを想定しているかが明白だと思います。
div
[ style "backg-color" "red"
, class "parent"
]
[ div
[ class "child0"
]
[ text "This is child0"
]
, div
[ class "child1"
]
[ text "This is child1"
, div
[ class "grandchild"
]
[ text "This is grandchild"
]
]
]
ではJSっぽい記法で同じものを表現するとどうなるでしょうか?
div(
[
style("background-color", "red"),
className("parent"),
], [
div(
[
className("child0"),
], [
text("This is child0"),
]
),
div(
[
className("child1"),
], [
text("This is child1"),
div(
[
className("grandchild"),
],[
text("This is grandchild"),
]
),
]
),
]
)
マジでぐっちゃぐちゃ!
これを避けるためにReactなどはjsxという独自の拡張記法を導入してツールチェーンを複雑にしていますが、Elmはそんな面倒なことをしなくてもこのカッコなしの記法のおかげでこんなにコードがすっきりするのです。
関数の定義
foo arg = baz (bar arg)
上記のElmコードは、ES6で表現すると以下の定義式と等価です。
const foo = arg => baz(bar(arg))
複雑な関数の場合は、以下のようにローカル関数を使うことができます。
foo arg =
let
x = bar arg
y = baz x
in
y
ちょっとシンタックスが見慣れないだけで、とくに難しくありませんね!
同じ型しか使えない配列
JSでは、以下のコードは文法として間違っていません。
as = [1, "string", true]
しかし、すべての値に対して特定の関数を適用する場合を考えると、決して優れたコードとはいえません。
as = [1, "string", true]
as.map(foo);
map
はもちろん普段から利用されていると思いますが、念のため補足すると、上記のプログラムは、以下とほぼ等価です。
as = [1, "string", true]
for (i = 0; i < as.length; i++) {
as[i] = foo(as[i]);
}
このコードは、foo
関数の引数として、
- 数字
- 文字列
- 真偽値
のどれでも与えられる場合にのみ正しく動きます。
でも、毎度foo
関数の引数になにを取りうることができるか、注意して配列に入れていい型を考えてプログラミングしないといけないなんて、難しくありませんか?
Elm は、この問題を解決するために、
配列に入れていいのは、同じ型の値だけ というルールを持っています。
ためしに、対話環境に以下の配列2を定義してみてください。
> ["foo", 2]
以下のように、丁寧なエラーメッセージで、教えてくれます。
The 1st and 2nd entries in this list are different types of values.
4| ["foo", 2]
^
The 1st entry has this type:
String
But the 2nd is:
number
ここでも、言語機能を絞ることで、初心者にもやさしく、さらに実際の開発でも保守性を高めてくれるようになっています。
まとめ
今回はElmの機能のうち、以下の3つについて見てみました。
- 変数の使い方
- 関数の使い方
- 配列の使い方
いずれも、多少シンタックスが変わる程度で、JSよりも機能がずっと少ないことに気づいていただけたと思います。
また、その少ない機能で、むしろJSよりもずっといいコードが書けそうな感覚を持ってもらえたのではないでしょうか。
ここで衝撃の事実を明かしますが、なんとこれはElmの魅力のほんの 1% なのです!
言語としての素晴らしさもまだまだありますが、そもそもElmがどうしてこのような言語仕様を採用したかを知ると、ますますElmの魅力に取り憑かれてしまうことでしょう。
興味がわいた方は、まず Elmはどんな人にオススメできないか を読んでElmの素晴らしい「哲学」を理解し、その上で つまづかないでElmを始めるために最初に読むページ を是非ご覧ください。