Elmはフロントエンド用の関数型言語です。良く触れたことが無い人が「関数型だから難しそう」「JavaScriptは少し書いたことがあるから、Angular, React, Vue等のフレームワークを」という印象を抱いてしまうようです。少なくとも関数型パラダイムへのシフトと言うところの負担があるかもしれませんが、それを含めても学習が困難であると言うことはありません。この記事では、Elmの全てが詰まっていると言っても過言ではないThe Elm Architecture(TEA)を一気に流し込んで、何だ簡単そうだぞ?Elmと言う印象を抱いてもらえればなと思います。
また、何となく分かったけどもっと、お行儀よく学びたい。と言う方は、こちらのロードマップをご覧ください。
TEAで学べること
さて、ElmでTEAを学ぶと何ができるようになるのでしょうか? 答えを言ってしまうと以下を学ぶことができます。
- イベントハンドリング
- 状態管理
- 副作用の分離
- 乱数
- Http
- スケジュール処理
- (JavaScriptとの通信)
いくつか並べましたが、簡単に言ってしまえばモダンWebフレームワークのお仕事のほとんど、もしくは追加で入るようなRxJSやRedux, Vuex等のライブラリ機能を包括しています。そう考えると一つの記事で収まるはずが無さそうだと思いますが、安心してください。TEAではほんの少しのお作法と関数呼び出しだけでこれが出来てしまいます。それでは実際に手を動かしながら学んでみてください。
環境構築
Elmの環境はnodejsをお手元のPCに入れ、次のリポジトリをクローン(elm-parcel)することで出来ます。
$ cd elm-parcel
$ npm i
$ npm start
// connect http://localhost:1234
コーディングは、VSCode(Elm Tooling)やお好きなエディタで行ってください。
落としてきたリポジトリを以下のような雛形に修正してください。二つのボタンが現れ、押しても何も反応がなければ成功です。
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, p, text)
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
-- MODEL
type alias Model =
String
init : () -> ( Model, Cmd Msg )
init _ =
( "", Cmd.none )
-- UPDATE
type Msg
= NoOp
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
( model, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [] [ text "hello" ]
, button [] [ text "world" ]
, p [] [ text model ]
]
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
Hello World
それでは、今回対象となるHello Worldをやっていきましょう。onClickイベントが起きたときに、Msgを発行します。発行されたMsgをハンドリングする場所がupdate関数です。JavaScriptで言うeventListnerのコールバック関数に当たる部分ですが、TEAではアプリケーションの状態(Model)を書き換えたり副作用を起こしたりすることができる核の一つとなります。今回は、Stringを一つ受け取ることができるChangeWordと言うMsgを作りました。Model(String)は、受け取ったwordで上書きされます。そのため、helloボタンでは"Hello"、worldボタンでは"World"に書き換えられ、Modelの変更の通知を受け取ったview関数がpタグの内容を書き換える。と言うプログラムになります。
+ import Html.Events exposing (onClick)
type Msg
- = NoOp
+ = ChangeWord String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
+ case msg of
+ ChangeWord word ->
+ ( word, Cmd.none )
view : Model -> Html Msg
view model =
div []
- [ button [] [ text "hello" ]
+ [ button [ onClick (ChangeWord "Hello") ] [ text "hello" ]
- , button [] [ text "world" ]
+ , button [ onClick (ChangeWord "World") ] [ text "world" ]
, p [] [ text model ]
]
乱数を使う
次に"random hello"ボタンを押すと、"Hello"と出したり"World"と出したりする乱数を用いたプログラムを書いてみましょう。
elm/randomパッケージが必要になります。ターミナルで以下のコマンドを実行してください。
$ npx elm install elm/random
Here is my plan:
Add:
elm/random 1.0.0
Would you like me to update your elm.json accordingly? [Y/n]: y
Success!
まず初めに、Randomモジュールをimportして使える状態にします。
"Hello"や"World"のどちらかの文字列をランダムに発生するトリガーとなるRandomHelloWorld Msgを追加します。Msgはパイプ(|)記号でいくつもMsgを増やすことができます。
次に実際にランダムに文字列を生成するrandomHelloWorld関数を定義します。ランダムな値を生成したり加工できたり型がGeneratorと言う形になります。こちらはランダムに発生する値の型変数(型パラメータ、いわゆるジェネリクスです)渡す必要があります。今回は文字列なのでStringとなります。uniformはリスト(配列)でランダムに発生する値を複数受け取ることができます。空配列などでランダムに発生する値が一つもないことを避けるために、引数が二つになっています(面白いインターフェースですね)。今回は、二択なので"Hello"と[ "World" ]をuniform関数に渡します。
RandomHelloWorld Msgの分岐では、キックされた瞬間は乱数を発生させていないためmodelは現状のままを渡します。代わりにタプルの第二引数((model, ここ))に、Random.generate関数に発生した値を受け取るためのMsgと先ほど定義したrandomHelloWorldを渡した結果を評価してあげます。Modelを書き換えるためには必ずupdate関数が呼び出されます。update関数は原則、Msgを経由して呼び出されます(update関数内でupdateを呼び出すなどの例外も存在します)。このとき副作用を起こして受け取る場合にはCmd msgと言う型がupdateの返すタプルの第二引数の型になります。
最後にボタンのonClickは、RandomHelloWorld Msgをキックして乱数を発生させます。
+ import Random
type Msg
= ChangeWord String
+ | RandomHelloWorld
+ randomHelloWorld : Random.Generator String
+ randomHelloWorld =
+ Random.uniform "Hello" [ "World" ]
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ChangeWord word ->
( word, Cmd.none )
+
+ RandomHelloWorld ->
+ ( model, Random.generate ChangeWord <| randomHelloWorld )
view model =
div []
- [ button [ onClick (ChangeWord "Hello") ] [ text "hello" ]
+ [ button [ onClick RandomHelloWorld ] [ text "random hello" ]
- , button [ onClick (ChangeWord "World") ] [ text "world" ]
, p [] [ text model ]
]
Http
Webアプリケーションでは欠かせないHttp通信を行う方法を押さえておきましょう。elm/httpとelm/jsonモジュールを使えるようにしておきます。
$ npx elm install elm/http
Here is my plan:
Add:
elm/bytes 1.0.8
elm/file 1.0.5
elm/http 2.0.0
Would you like me to update your elm.json accordingly? [Y/n]: y
Success!
$ npx elm install elm/json
I found it in your elm.json file, but in the "indirect" dependencies.
Should I move it into "direct" dependencies for more general use? [Y/n]: y
Success!
今回は私が用意したGistのhello.txtの中の文字列をHttp通信で受け取ってみましょう。(以下のGistが消失していましたら、ご自分のGistで代替してください。)
こちらのGist API(https://api.github.com/gists/3a2cdeb411d654ff80d72b400e327a68
)をGetメソッドで叩くことで以下のJSONを取得できます。res.files["hello.txt"].content
と潜っていった先がお目当ての値になります。
{
...
"files": {
"hello.txt": {
"filename": "hello.txt",
"type": "text/plain",
"language": "Text",
"raw_url": "https://gist.githubusercontent.com/ababup1192/3a2cdeb411d654ff80d72b400e327a68/raw/b45ef6fec89518d314f546fd6c3025367b721684/hello.txt",
"size": 13,
"truncated": false,
"content": "Hello, World!"
}
},
...
それでは、コードを書き換えていきましょう。
HttpとJsonDecoder(JD)を使える状態にしてあげます。
受け取った値でそのまま書き換えるので、ChangeWordは御役御免となります。Randomのときと同様、HTTP通信を行う処理をキックするためのMsg GetHelloWorldと通信結果を受け取る * GotHelloWorld* Msgを追加しましょう。Elmでは実行時エラーを起こさないようにするためにエラーハンドリングがある場合は必ずそれ相当の形になります。HTTP通信は、ネットワーク障害やBadRequestなどのエラーが発生する可能性があるため、*Result (Http.Error 受け取る結果)*と言う型になります。
GetHelloWorldは、HTTP通信を行うgetHelloWorld関数を呼び出すだけです(乱数のRandomHelloと同じです)。GotHelloWorldは通信結果を受け取ります。エラーハンドリングの分岐が入ります。Okが成功パターン、Errが失敗パターンです。Okで受けとれたcontentでModelを上書きします。Errの場合はエラー内容を無視して"Error"と言う文字でModelを更新します。Wifiをオフにして、Chromeのシークレットウィンドウ等でこの文字は確認できると思います。実際のアプリケーション開発では必ずエラーは無視せずにフィードバックをユーザに返してあげましょう。
getHelloWorldはHTTP通信を定義する関数です。Random.generateと同じ結果の型になります。GETメソッドで、GistのAPIを使用します。expectではどのように結果を受け取りたいかを定義します。今回は、JSONで受け取りGotHelloWorldで結果のMsg受け取ります。JSONをDecodeしてElmのStringに変換しなければならないため、gistDecoderを定義して渡します。
gistDecoderは、この説の冒頭で書いたAPIのレスポンスをDecodeする関数になります。今回は単にオブジェクトを掘っていくだけで事足りるため、at関数でキーをリストで渡していくだけです。最終結果は、JD.string関数で、ElmのStringに変換してあげます。
View関数は、onClick GetHelloWorld。(もう解説しなくても良さそうですね)
- import Random
+ import Http
+ import Json.Decode as JD
type Msg
- = ChangeWord String
+ = GetHelloWorld
- | RandomHelloWorld
+ | GotHelloWorld (Result Http.Error String)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
- ChangeWord word ->
- ( word, Cmd.none )
-
- RandomHelloWorld ->
- ( model, Random.generate ChangeWord <| randomHelloWorld )
+ GetHelloWorld ->
+ ( model, getHelloWorld )
+
+ GotHelloWorld result ->
+ case result of
+ Ok content ->
+ ( content, Cmd.none )
+
+ Err _ ->
+ ( "Error", Cmd.none )
+ getHelloWorld : Cmd Msg
+ getHelloWorld =
+ Http.get
+ { url = "https://api.github.com/gists/3a2cdeb411d654ff80d72b400e327a68"
+ , expect = Http.expectJson GotHelloWorld gistDecoder
+ }
+ gistDecoder : JD.Decoder String
+ gistDecoder =
+ JD.at [ "files", "hello.txt", "content" ] JD.string
view : Model -> Html Msg
view model =
div []
- [ button [ onClick RandomHelloWorld ] [ text "random hello" ]
+ [ button [ onClick GetHelloWorld ] [ text "gist hello" ]
, p [] [ text model ]
]
スケジューリング
今度は1秒ごとに、"Hello"と"World"がチカチカと入れ替わるLチカならぬ、Hello Worldチカチカをやってみたいと思います。
時間に関するモジュールを入れます。
$ npx elm install elm/time
I found it in your elm.json file, but in the "indirect" dependencies.
Should I move it into "direct" dependencies for more general use? [Y/n]: y
Success!
importします。
Tickは現在の時刻をPosixタイムで受け取り発火されるMsgです。今回受け取った時間は捨て使いません。
Tickの分岐では、空文字(初期状態)もしくは、"Hello"であれば、"World"に、"World"であれば"Hello"になりModelを更新していきます。これでチカチカできそうですね?
view関数はもはやボタンが無くなり、pタグだけになってしまいました。
今まで触れて来なかったsubscriptions関数はブラウザを監視し続けて、時間であったりグローバルなマウスイベントを検知し、Msg(update)を発火するための関数になります。every関数は時間(ms)を受け取り、その時間おきにMsgを発火させます。今回は、1秒おきにTick(チカチカ)させます。
- import Html.Events exposing (onClick)
- import Http
- import Json.Decode as JD
+ import Time
type Msg
- = GetHelloWorld
- | GotHelloWorld (Result Http.Error String)
+ = Tick Time.Posix
update msg model =
case msg of
- GetHelloWorld ->
- ( model, getHelloWorld )
-
- GotHelloWorld result ->
- case result of
- Ok content ->
- ( content, Cmd.none )
-
- Err _ ->
- ( "Error", Cmd.none )
+ Tick _ ->
+ if String.isEmpty model || model == "World" then
+ ( "Hello", Cmd.none )
+
+ else
+ ( "World", Cmd.none )
view : Model -> Html Msg
view model =
div []
- [ button [ onClick RandomHelloWorld ] [ text "random hello" ]
[ p [] [ text model ]
]
subscriptions : Model -> Sub Msg
subscriptions _ =
- Sub.none
+ Time.every 1000 Tick
updateのTickの分岐をリファクタリングしたい気持ちになったので、リファクタリングしておきます。
( if String.isEmpty model || model == "World" then
"Hello"
else
"World"
, Cmd.none
)
まとめ
一気に駆け抜けましたがどうでしたでしょうか? 冗談抜きでTEAの一通りの説明が完了になります。細かい文法や仕組みの理解は置いておくとして、アプリケーションを作るイメージが湧いたのではないでしょうか? それほどまでにElm(TEA)の仕組みはシンプルであり、簡単です。Webアプリケーションのための必要なほとんどの機能に触れルことができましたが、「関数型の匂い」を感じたでしょうか? もし、あまり抵抗がなくなんとなくの理解が出来た方は是非Elmを触れ始めてみてください。(と言ってもこれ以上の目新しさは、あまり出てきませんが・・・) きっと簡単にWebアプリケーションを作ることができるでしょう。それでは、良いElmライフを!