(注)このハンズオン資料は記載時点の最新版 Elm 0.19 を対象としています。
ブラウザだけでElmをはじめる
Elm を体験するだけなら、ローカルマシンにインストールは必要ありません。
Ellieを開く ※初回アクセス時は利用規約への同意(ACCEPT TERMS)を行ってください
EllieはElmコードをコンパイルしてプレビューしてくれるオンラインサービスです。
今回のような体験にも使えますし、外部パッケージをインストールすることもできるのでちょっとした動作確認に使えます。
Ellie の画面構成
- Elmのコードを記述する部分
- HTMLのコードを記述する部分
- プレビューを表示する部分
に分かれています。今回のハンズオンではHTMLを記述する部分は変更しませんので、HTML記述部分の右上の矢印で閉じておいても良いでしょう。
(optional) フォントを変えても良い
左上の歯車⚙アイコンで見た目などの変更ができます。フォント(サイズや種類)はご自身で使いやすいものを選択しましょう。
最初に表示されるサンプル
Ellieの初期画面の状態でコンパイルしてプレビューが表示されている状態です。
Ellieで最初に表示されるのはシンプルなカウントアップ・カウントダウンができるアプリケーションです。
初期状態は数値が 0 になっており、+1ボタンをクリックすると値が増え、-1ボタンをクリックすると値が減ります。
ボタンをポチポチ触ってみましょう。
プレビュー部分のブラウザの上部にある「RELOAD」ボタンを押してみましょう。ブラウザがリロードされる挙動を再現しており、カウントが0に戻ります。
サンプルコードをコンパイルする
サンプルコードをすこしいじってみてコンパイルしてみましょう。
まずは、 initialModel
を変更し、 count = 100
にしてみます。
initialModel : Model
initialModel =
{ count = 100 }
この時点では自動的にはコンパイルされません。
「COMPILE」ボタンを押してコンパイルしてみましょう。
するとプレビューが更新されて表示される値が100になったはずです。
RELOADを押しても最初に表示される状態が 100 になっています。
Hello, world
定番ネタの Hello, world!
を表示してみましょう。
さて、どこをいじればこのような表示ができるでしょうか?
画面表示に関する部分がどこにあるのか、推測してみてください。
答えは view
の部分になります。次のようにコードを変更してみましょう。
view : Model -> Html Msg
view model =
div []
[ button [ onClick Increment ] [ text "+1" ]
, div [] [ text <| String.fromInt model.count ]
, button [ onClick Decrement ] [ text "-1" ]
, div [] [ text "Hello, world!" ]
]
(Elmで推奨されるコードフォーマットはやや見慣れないかもしれません。右上にフォーマットボタンもあるのでぜひ使ってみてください)
view
という名前なので追記する場所についてはわかりやすいかもしれませんが、追加すべきコードはまだわからないかもしれません。
追加した部分 div [] [ text "Hello, world!" ]
で生成されるHTMLはこのようになります。
<div>Hello, world!</div>
記述方法は違いますがなんとなくHTMLを記述していることは納得できるでしょうか。
挙動を変えてみる
ここまで、初期状態(initialModel
)を変えて、HTMLの記述(view
)を変えました。
続いてアプリケーションの動作を変更してみましょう。
ひとまず、 +1 を +2 にしてみたいと思います。
view : Model -> Html Msg
view model =
div []
[ button [ onClick Increment ] [ text "+2" ]
, div [] [ text <| String.fromInt model.count ]
, button [ onClick Decrement ] [ text "-1" ]
, div [] [ text "Hello, world!" ]
]
画面上は +2ボタンになりました。しかし、挙動は相変わらず +1 のままです。
実際の挙動を管理しているのは update
の部分です。
値を修正してみましょう。
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 2 }
Decrement ->
{ model | count = model.count - 1 }
コンパイルすると値が正しく +2 されるようになりましたか?
機能を追加してみる
ここまでは既存のコードの値を変更するだけでした。
次はボタンを追加して機能を増やしてみましょう。
値をリセットするのに毎回RELOADを実行するのは不便ですよね。
そこで、リセットボタンを追加してみることにします。
リセットボタンを表示する
まずはボタンを表示します。既存のボタンを真似て、コードを追加してみましょう。
なお、十分役目を終えた Hello, world!
には消えてもらうことにします。
view : Model -> Html Msg
view model =
div []
[ button [ onClick Increment ] [ text "+2" ]
, div [] [ text <| String.fromInt model.count ]
, button [ onClick Decrement ] [ text "-1" ]
, button [] [ text "reset" ]
]
修正後にコンパイルすると、ややボタンの位置が不格好ではあるものの、HTMLの記述通りの状態になります。
まずは機能が動作することを確認したいので、このまま進めます。
このボタンは表示上追加しただけなので、ボタンを押してもリセットはされません。
「リセットする」指示を定義する
「リセットする」というアプリケーションの挙動を定義します。
まずは、アプリケーション上の指示を Msg
に追加します。
この Msg
は代数型と呼ばれ、 Increment
, Decrement
, Reset
という定数のどれかになる(それ以外はありえない)ことを定義しています。
type Msg
= Increment
| Decrement
| Reset
この段階でコンパイルしてみましょう。コンパイルエラーが発生するはずです。
Elm の出力するエラーは非常に丁寧です。エラーが発生した場所だけでなく、どのような修正を行うべきかまである程度教えてくれます。
今回の場合、 Msg
の種類を増やしたのに、 Msg
を扱っている update
の方では Increment
と Decrement
の2パターンのみに対応する方法しか記載していないので、 Reset
に対応する部分が抜けていますよ、ということを示しています。
では、 update
を Reset
に対応させましょう。
「初期状態に戻す」ということはどのように記述すれば良いでしょうか?
すぐに思いつく方法は、他の例に倣って count = 100
と記述する方法だと思います。
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 2 }
Decrement ->
{ model | count = model.count - 1 }
Reset ->
{ model | count = 100 }
人によっては 100 を定数化したい、などイライラするかもしれませんが、ひとまずはこれで大丈夫です。
まだこの { | }
という記法の読み方も記載していませんので…。
ひとまずこれでコンパイルエラーは解消されました。しかし、まだボタンは動作しません。
ボタンの挙動に機能を設定する
最後に、ボタンを押したら機能を呼び出すようにしてみましょう。
再び view
の方を修正します。
view : Model -> Html Msg
view model =
div []
[ button [ onClick Increment ] [ text "+2" ]
, div [] [ text <| String.fromInt model.count ]
, button [ onClick Decrement ] [ text "-1" ]
, button [ onClick Reset ] [ text "reset" ]
]
onClick Reset
を追加しました。
これは、<button onClick="...">
に相当するものですが、JavaScriptでは「処理」を記述するのに対して、 Elm では Reset
という値を送っています。
ここまでのコードはこちらからも参照できます -> https://ellie-app.com/nhq5QTrNMpHa1
Elm の動作する仕組み
ここまで部分的に変更してきましたが、それぞれの部分がどのようなつながりを持って動いているのか理解しておきましょう。
Elm は The Elm Architecture と呼ばれるアプリケーションの骨組みを持っています。
これは基本的に3つのものから成り立っていて、
- Model : アプリケーションの持つ内部状態を表現する
- View : ModelからHTMLを生成する関数
- Update : Msg を受け取って、Modelを更新する関数
だけです。
ここで、「Modelを更新する」とサラッと書きましたが、ここが関数型言語の気にすべきポイントです。
Elmでは全ての値は不変(immutable)であるという特徴があるため、実際には「更新している」のではなく「新しいModelの値を作っている」ということになります。
レコード
Model
は次のように定義されています。
type alias Model =
{ count : Int }
type alias
とは型の別名を定義する文法です。ここでは =
の右である { count : Int }
という型に Model
という別名を定義しています。
では、 { count : Int }
は何かというと「レコード」という型です。JavaScriptでいうオブジェクトに近いです。
レコードには count
という「フィールド」が宣言され、中身は Int
型のデータが入ることが定義されています。
レコード型の値は initialModel
で作っています。
initialModel : Model
initialModel =
{ count = 100 }
定義時は { count : Int }
のようにコロンで定義していましたが、作るときは { count = 100 }
とイコールになることに注意しましょう。
レコードの更新
先程のリセットの処理では count
の値を設定するだけなので、次のように記述しても動作します。
Reset ->
{ count = 100 }
しかし、この方法ではもしフィールドが複数ある場合に面倒なことが起こります。
すでに述べたように Elm では既存のmodelの値やフィールドの変更はできず、「新しい値」として生成する必要があるので変更しないフィールドまで全て列挙しなくてはならず大変なことになります。
{ count = 100, hoge = model.hoge, fuga = model.fuga, ... }
そこで、レコードの更新には便利な方法があります。「フィールドアクセス関数」と呼ばれる便利な記法です。
ベースとなるレコードを |
の左に置き、右には変更したい部分のフィールドの値を記述します。
{ model | count = 100 }
これでフィールドが増えても部分更新がしやすいので助かりますね。
デバッグモードを見てみる
値が不変であり、新しいModelの値を毎回生成していることを少し実感してみましょう。
Elmにはデバッグモードでのビルドが標準で存在します。 Ellie 上では右上に DEBUG ボタンがあるので開いてみましょう。
デバッグモードではMsgが発生するごとに履歴が一つずつ生成されており、そのMsgを処理した状態のModelに戻すことができます。
このように毎回Modelを新しいModelの値に置き換えることでアプリケーションが動作していることがわかります。
型・シグネチャ
さて、少し関数型言語の側面を紹介していきます。
これまで説明をしていなかった main
関数を見てみましょう。
main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}
Browser.sandbox
というのは The Elm Architecture のためのエントリーポイントの中で一番シンプルなものです。ここでは詳しく説明しませんが、他にも Browser.element
, Browser.document
, Browser.application
などがあり、より高機能なアプリケーション開発を行う際は必要になります。
Browser.sandbox
も関数です。公式ドキュメントを見ると次のように記載されています。
sandbox :
{ init : model
, view : model -> Html msg
, update : msg -> model -> model
}
-> Program () model msg
これまでの view
や update
のような関数の1行目にもありましたが、こちらが「型シグネチャ」と呼ばれるものです。
sandbox
は「 init
, view
, update
というフィールドを持つレコード」を受け取って、「 Program () model msg
型の値」を返します。
最初の Model で見たレコードの定義ではフィールドが count : Int
だったので整数値でしたが、 view : model -> Html msg
というように、ここにもシグネチャのようなものがあります。
これはすなわち「関数」を値として受け取ることを意味しています。
このように、「関数を値として扱うこと」を 第一級関数(first-class function) と言います。関数型言語ではこれが前提となる考え方の一つとなります。
関数を返す関数
では、これまで見てきた initialModel
, view
, update
の型シグネチャを見てみましょう。
initialModel : Model
view : Model -> Html Msg
update : Msg -> Model -> Model
さきほどの sandbox
のシグネチャとは大文字小文字が違いますが、小文字で定義したほうは「 model
の部分には同じ型が入る」というもので「型引数」といい、大文字で始まる Model
が具体的に定義されたものです。
update
のシグネチャは矢印 (->
) が2つあります。これは Elm 上はこのように解釈されます。
update : Msg -> ( Model -> Model )
Model -> Model
は Model
を受け取って Model
を返す関数を表しています。
つまり、 update
関数は Msg
を受け取って、「モデルの値を新しいモデルにして返す関数」を返り値として返していることになります。
ところが、現在の update
関数は2つの値を受け取るかのように記述されています。
update : Msg -> Model -> Model
update msg model =
...
このように2引数の関数のように定義することもできます。
ちょっと回りくどくなりますが、今回はこれを実際に「関数を返す関数」に分解してみましょう。
まず、3つの関数を定義します。
increment : Model -> Model
increment model =
{ model | count = model.count + 2 }
decrement : Model -> Model
decrement model =
{ model | count = model.count - 1 }
reset : Model -> Model
reset model =
{ model | count = 100 }
これらはすべて Model -> Model
の型シグネチャを持っています。
続いて、 update
関数を変更してみましょう。
update : Msg -> Model -> Model
update msg =
case msg of
Increment ->
increment
Decrement ->
decrement
Reset ->
reset
引数が msg
だけになり、 Msg
の各パターンに応じて関数を返すようになりました。
これでも正しく動作することを確認しましょう。