JavaScript
入門
Elm
ElmDay 15

JavaScriptよりも初心者向け? かんたんElm入門(文法基礎編)

More than 1 year has passed since last update.

はじめに

Elmという名前を聞いたことがありますか?
Reduxを使ったことがある方は、「ああ、Reduxのもとになった言語ね!」と思ったかもしれません。
世間では、「Elmは設計思想としてはいいけど、Haskellの知識がないとわからないし難しい」みたいな嘘にだまされて、「JSの知識だけで使えるし、簡単なReduxの方を使ってみよう!」と思う方も少なくないようです。

しかし、Elmは難しくありません。
Elmは、初心者でも簡単に使える言語であり、かつ実業務で採用してもJSで書くよりも保守性や拡張性が高いコードを書くことができます。
実際に、最近リリースされたバージョン0.18では、機能が追加されるどころか、「この機能は紛らわしい」「この機能は別の機能で代替できる」という理由で削られ、どんどんシンプルになっていっています。
(Elmの興味深い思想についてもっと知りたい方はElmはどんな人にオススメできないかをご覧ください)

いままでJSを書いてきた方にとって、Elm は新しい機能の使い方をおぼえる必要はほとんどなく、JSの知識だけでも理解できる文法です。
また、要求されるJSのレベルも、それほど高い知識は求められません。
たとえば私は1日10時間以上寝ないと頭が働かない体質の上、会社の経営や、バックエンド側のプログラミング業務などにより、あまりフロントエンドの学習に時間を割くことができない中、片手間にJSを学習して使っていた程度のレベルです。
むしろ、そういう時間が限られた環境だからこそ、学習コストもJSに比べて低く、保守性と拡張性の高いプログラムを書けるElmを愛用しています。

busy

この記事では、Elmの

  • ためし方
  • 変数の使い方
  • 関数の使い方
  • 配列の使い方

について、JSとの差異に触れながら紹介していきます。

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を呼んだことにより、グローバル変数のx4がセットされ、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)

とするのと等価です。
カッコをつけないのと、引数の区切りを,ではなく半角スペースにしているところに差異がありますが、特に本質的な違いはありませんね?

関数の定義

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よりもずっといいコードが書けそうな感覚を持ってもらえたのではないでしょうか。
興味を持たれた方は、ぜひ公式ドキュメントをご覧ください。

次回以降、

  • If文
  • タプル
  • レコード
  • 型表記
  • Elm Architecture

について紹介したいと思います。


  1. もちろん、ESLintなどを導入すれば防げますが、「JS界隈はツールがいっぱいでわかりにくい」と言われてしまいます。 

  2. 厳密にはリストですが。