Elmとは
- フロントエンド開発に特化したプログラミング言語
- JavaScriptにコンパイルされて実行される、いわゆるAltJS
- 2012年に登場した、比較的若い言語
- 静的型付言語&純粋関数型言語
- 同じく純粋関数型言語であるHaskellの子孫
(ElmのコンパイラはHaskellで書かれています)
Elmはフレームワーク内蔵言語
React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。
ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済みます。そのため、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。
JavaScriptとの違い
変数について 〜JavaScriptの場合〜
JavaScriptでは、var
かlet
で宣言された変数には後から別の値を再代入することが出来ますが、const
で宣言された定数には再代入ができません。
var a = 1;
a = 2;
let b = 1;
b = 2;
const c = 1;
c = 2; // エラー!
変数について 〜Elmの場合〜
Elmの場合はvar
もlet
もconst
もありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。
a = 1
a = 2
-- コンパイルエラー!
そもそも代入や再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。
オブジェクト 〜JavaScriptの場合〜
const
で宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。
const takashi = {
name: "たかし",
age: 36,
};
takashi.age = 37; // ageプロパティを上書き。
レコード 〜Elmの場合〜
Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。
takashi =
{ name = "たかし"
, age = 36
}
newTakashi =
{ takashi | age = 37 } -- 上書きでなく新しいレコードが生成されます。
レコードは、JSのオブジェクトと異なり完全にイミュータブル(不変)なため、ageだけ上書きするということはできません。
上記のコードも、一部を変えた新しいレコードが作り出されます。
元のレコード(takashi)は36歳のまま残ります。
書く順序による影響 〜JavaScriptの場合〜
↓エラーにならないパターン
const a = 3;
const b = 5;
const c = a + b;
↓エラーになるパターン
const c = a + b;
const a = 3;
const b = 5;
a
やb
に値を代入するより上の行で、a
やb
を使った計算などをしようとするとエラーになります。
書く順序による影響 〜Elmの場合〜
c = a + b
a = 3
b = 5
↑a
やb
を定義するより上の行でa
やb
を計算に使うことができます。
JavaScriptでは状態の変化を直接コードに書くことができる
let takashi_age = 36;
console.log(takashi_age); // 36
takashi_age = 37;
console.log(takashi_age); // 37
takashi_age = 38;
console.log(takashi_age); // 38
takashi_age = 39;
console.log(takashi_age); // 39
JavaScriptの場合は変数の値を変更できるので、例えばconsole.log
をtakashi_age = 〇〇;
の上の行に書くか下の行に書くかが重要です。
Elmでは、場面や状態の変化を直接コードで書けない
再代入という概念がなく、コード内の全ての値が不変だからです。
season = "夏"
-- ずーっと夏
age = 36
-- ずーっと36歳
そのため「この行でageを呼び出したら36
だけど、もう少し下の行で呼び出したら37
だった」ということがありません。
再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
つまり、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行で呼び出すことも可能です。
再代入できないことによるメリット
- 違うものには違う名前がつくので明解
- この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
- 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
- 値の中身がコロコロ変わらないので、QiitaやGitHubでソースを読むときも少し楽
関数 〜JavaScriptの場合〜
JavaScriptでは↓こんな感じで関数を宣言します。
function add (a, b) {
return a + b;
}
また、無名関数を変数に格納するパターンもあります。
const add = function (a, b) {
return a + b;
}
アロー関数式で書く場合は↓こうです。
const add = (a, b) => a + b;
実行するときは↓こうですね。
const result = add(3, 5);
関数 〜Elmの場合〜
Elmでは↓こんな感じで関数を定義します。
add a b =
a + b
実行するときは
result = add 3 5
Elmではカッコもカンマも必要ありません。
戻り値を返すのにreturn
を書く必要もありません。
関数と変数の境目があまりない感じです。
引数があれば関数です。
関数の返す値も(引数が同じならば)常に一定
Elmの関数は、引数が同じであれば何度実行しても同じ値を返します。
場面によって戻り値が変わるような関数は書けないイメージです。
それは、全ての値やレコードが不変で、場面や状態といったものを直接コードで書くことができないからです。
scene =
"朝"
greeting name =
if scene == "朝" then
name ++ "さん、おはよう!"
else
name ++ "さん、こんにちわ!"
この関数をgreeting "たかし"
と実行した場合、戻り値は常に"たかしさん、おはよう!"
となります。
Elmでは再代入ができないため、scene
は常に"朝"
だからです。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態は不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。
Elmは何も変えられない・・・?
Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数を書くときにも「関数の中から、何か外部の値を変えてくれい!」という命令的なコードを書くことはできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けないイメージです。
具体的には、JavaScriptでいう、
doSomething(a, b);
的な「戻り値を使わない」コードは書けません。
doSomething
関数の中から外部のものを変えるすべが無いため、戻り値を受け取って利用しないと何も出来ないのです。
関数の中から唯一外の世界に影響を与えられるのは戻り値だけです。
実行だけして戻り値を使わない、というコードを書くことはありません。
const result = someCalculate(a, b);
上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」という定義をするような書き方がメインになります。
何も変えられなくて、この言語なにができるの?
定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。
しかし「Webアプリケーションを作る」ということは「ユーザがどんな行動をしたら、Webアプリの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。
デモアプリをみてみましょう
デモアプリ
ブラウザエディタEllieでご確認ください。
※好きにコードをいじってもらって大丈夫です!
(私のコードとは別に保存されるので)
定義だけで状態変化を表現できました
しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。
オブジェクト指向との違い
オブジェクト指向とは
データと関数(メソッド)をまとめて定義した「クラス」つまり種を定義して、そこから実体となるオブジェクトを生成する。
人間が現実世界のモノを認識するときの考え方に似ているため、比較的直感的にプログラミングできるという、非常に強力なスタイル。
※諸説ありますが、ここでは上記の認識で進めます。
クラスを使って書いてみる
class Human {
constructor (name, age) {
this.name = name;
this.age = age;
}
increment () {
this.age++;
}
decrement () {
this.age--;
}
}
const takashi = new Human("たかし", 36);
takashi.increment();
takashi.increment();
console.log(takashi.age); // 38
関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然なコードになります。
Elmでも、オブジェクト指向っぽく考えることもできる
ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。
addAge : Int -> Human -> Human -- 型注釈
addAge int human =
{ human | age = human.age + int }
Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。
コンポーネント指向との違いは?
div
やbutton
などのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。
buttonComponent props =
button [ class "common-btn" ] [ text props.text ]
これってReactやVueのFunctional Componentですよね。
ReactやVueでは、状態を持ったコンポーネントを作ることもよくありますが、最近は「純粋な関数型コンポーネントをメインにした方が、色んなところに状態が散らばらなくて良いかも・・・!」という意見もよく聞きます。
The Elm Architectureについて
ReduxやVuexの元となった手法
model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。
コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。
ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。
再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。
もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。
そして、環境構築のコストもかなり低めです。
型システムが優秀なため、修正にも強い
例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。
リセット機能追加の流れ
- リセットボタンをview関数の中に追加
- そのボタンをクリックしたら
Reset
というメッセージが発生するように属性を追加 -
Reset
なんて知らないよ!とエラーが出る - メッセージの型に
Reset
を追加 -
Reset
のケースも書かないと!とエラーが出る
エラーメッセージが分かりやすいのもElmの特徴です。特に、機能追加をしている時などは「そこにコードを追加するなら、ここにも追加しないとでは!?」と導いてくれているような感じがします。
エラーになるようなコードはコンパイル時に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。
純粋関数型言語の特徴
副作用を直接書けない
再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。
再代入によって値を変えることで状態変化を表現することはできないため、状態を変化させたい値はmodel
に組み込むことになります。
そして、状態を更新する処理はupdate
関数の中に書いていきます。
コードの記述方法がある程度定まっていることで「この処理は、この辺りに書いてあるだろう」と予測しやすくなるというメリットがあります。
参照透過性が担保されている
Elmの関数は、外部の値に依存して振る舞いを変えません。引数が同じであれば、同じ戻り値を返します。
それにより、単体テスト・自動テストがしやすくなります。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。
例えば、期限付きのタスク管理システム
↑のテストをする場合のことを考えてみます。
- タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
- その後Webサイト側で新規タスクを作る(期限日も設定)
- 時間操作の画面で日にちを進める。
- そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
- ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。
これだとテストしづらいですよね。。。
条件は全て引数として渡す
「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、自動テストが書きやすくなります。
テストの自動化
例えばJest等のテストフレームワークで、いくつも引数を変えながら関数の自動テストをするようにテストコードを書いてきます。
gitコミットをするたびにそのテストが走るように設定しておきます。
そうしておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、すぐにテストでエラーが出て気づけるので安心ですよね。
そのためには、引数の渡し方だけでテストを網羅できるようにしておく必要があり、日時や場面といった外部の状態に依存しない参照透過的な関数にしておくことが重要です。
Elmなら参照透過的な関数しか書けないため、TDDとの相性も良いです。
ReduxやVuexとの比較
ReduxやVuexはJSで書ける
ReduxやVuexはJavaScript製の状態管理フレームワークです。
Elmという新言語を学ぶのに比べると、ReduxやVuexは慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「この関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。
Elmなら
Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、JSだとどうしてもコード量が増えてしまう状態管理系の機能もサクッと実装できます。
しかも、初見で機能追加できてしまうくらいシンプルです。
最後に
Elmは難しいどころか、とても入門しやすく、楽しい言語です。
もちろん、複雑なものを作ろうとすればそれなりに難しいんですが、少し勉強したらピンポンゲームやシングルページアプリケーションをザコーダーの私でも作ることができました。
まだまだ紹介しきれていない魅力が本当にたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)