Edited at
ElmDay 19

ElmでSPAなサービスを作ったので、そのあたりを整理してみる (Elm v0.19)

この記事は、

です


Elmについて

皆さん、関数指向で型チェックがしっかりしている言語は好きですか?

自分は好きです

もしあなたもそんな言語が好きなら、きっとElmが好きになれます

ElmはAltJSの一種であり、Webのフロントサイドのフレームワークでもあります

特徴として、厳密な静的型チェック、強力なランタイム(Elm Runtime)に支えられた堅牢な構成(Elm Architecture)、HaskellやML系言語風のシンタックスが挙げられます

自分が「なぜElmでWebのフロントをやるのか」と問われると、真っ先に挙げるのはこの3つです


  • ビルドが通れば落ちない


    • かれこれ半年以上Elm書いてますが、実行時にエラーが起こったことは一度もありません



  • Elmの作法に則って書けば、とにかく動くものが作れる


    • 強力なランタイムと設計におかげ



  • 関数指向な考え方が好き


    • これには個人差があります



そんなElmであるサービスを作って、SPAなElmアプリケーションの構成をなんとなく理解できたので、その整理がてら書いていきます


注意

自分が作ったサービスはまだv0.18系です(現在v0.19に移行中)

なので、実は自分はv0.19でSPAな何かを作ったことがありません

ただ、v0.18と根本の考え方は同じなので、v0.18での知見をベースにいろいろ書いていきます

また、対象として「チュートリアルはやったけど、より深く理解したい...」という人を対象にしています

すでにバリバリコードを書いてる方はつまらないと思いますし、今さっきElmを知りました!という方はよくわからないと思います

また、subscriptionsとviewは今回の話とあまり関係がないため、この記事では省略します


ElmでSPA

ここでは、例としてこのページで例として出されているコードを元に書いていきます


main関数

C言語のプログラムはmain関数から始まります

(HaskellやRustなどほかの言語もそうですね)

Elmも、main関数から始まります

main : Program () Model Msg

main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}

サンプルコードのmain関数ですが、このような定義となっています

ここで、プログラマが定義した各種関数を登録してランタイム側に渡しています

main関数自体は特に説明する要素がないので、ここは飛ばしていきます

v0.19から、onUrlChangeonUrlRequestが追加されて、とても便利になりましたねー


init関数

type alias Model =

{ key : Nav.Key
, url : Url.Url
}

init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url, Cmd.none )

init関数は初期状態、つまり、ユーザがページにアクセスして各種リソースがロードされてElmの処理が走り始めた際の最初のアプリケーションの状態を定義されています

Model型はアプリケーション全体の状態を表す型です(プログラマが自由に決めていく)

init関数ですが、

init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )

という型定義になっています

第一引数はユニット型(()、ほかの言語でいうnullみたいなもの)になっていますが、これはElmアプリケーションの実行開始時にElmの世界の外からデータを受け取ることが出来るというものです(今回は使わないのでユニット)

記事の趣旨から外れるので、ここでは説明しません

第二引数と第三引数ですが、これはelm/urlとelm/browserというパッケージで定義されている型です

Urlはelm/urlというパッケージで定義されていて、その中身はこんな感じです

type alias Url =

{ protocol : Protocol
, host : String
, port_ : Maybe Int
, path : String
, query : Maybe String
, fragment : Maybe String
}

見たらわかる通り、単にURLの定義通りに情報を保持しているレコード型です

続いてKeyですが、これはブラックボックスとなっていて中身がわかりません

ただ、このKeyの値は、SPAで重要なルーティングに関する処理で必要となるので、Model内で保持しています

(サンプルコードではUrlの値もModelで保持してますが、これはSPAを実現する上では必要ないです)

このUrlKeyがどう使われるかは、後でupdate関数で見ていきます


update関数 (onUrlChange/onUrlRequest)

type Msg

= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )

Browser.External href ->
( model, Nav.load href )

UrlChanged url ->
( { model | url = url }
, Cmd.none
)

update関数は、アプリケーション全体の全ての動きを定義する関数です

型に注目してみると、こんな感じになっています

update : Msg -> Model -> ( Model, Cmd Msg )

MsgModelはプログラマが定義した型で、それぞれ、「アプリケーションの取りうる動き」と「アプリケーションの状態」を表す型です

つまり、updateMsg(次のアプリケーションの動き)とModel(今のアプリケーションの状態)を受け取って、Model(次のアプリケーションの状態)とCmd Msg(ランタイムへの副作用のある操作の依頼)を返す関数と言えます

また、ここで重要なのが、ElmにおいてはURLの変更もこのupdate関数を通して行われるという点です

main関数にonUrlChangeonUrlRequestとして渡したLinkClickedUrlChangedですが、この二つはMsgを返す関数とも見ることができ、その型は

LinkClicked : LinkClicked Browser.UrlRequest -> Msg

UrlChanged : Url.Url -> Msg

と見ることができます

UrlRequestはelm/browserというパッケージで定義されている型で、その定義は

type UrlRequest

= Internal Url.Url
| External String

となっています

それぞれ、Internalはドメインに対してローカルなURL(<a href="/home">リンク</a>)、Externalはドメイン込みのURL(<a href="example.com/home">リンク/a>)を表していています

URLの形式が違うものの、どちらもリンク先を表すという意味では同じです

また、Urlは上でも説明したように単にURLを表す型です

なぜかInternalExternalUrlStringと違う形になっていますが、これはElm側で用意されているnavigation系の関数(SPAでのページ遷移を実現する関数)がUrlを要求するようになっているためと思われます

また、SPAではなく通常のロードが発生するページ遷移は移動先のURLを文字列で要求するようになっていて、それを意識してあえてInternalExternalを分けているようです

話を戻して、LinkClickedonUrlRequestとしてmain関数を通してランタイムに渡されており、これはユーザがリンクをクリックした際に呼び出されます

(つまり、Browser.applicationはアプリケーション内でリンクがクリックされても、すぐには遷移しない)

サンプルコードでは、update内で

LinkClicked urlRequest ->

case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )

Browser.External href ->
( model, Nav.load href )

という風に処理をしていますが、Nav.pushUrlはSPAでのページ遷移、Nav.loadはロードを伴う遷移を提供する関数です

なので、このサンプルコードではInternalなリンクをクリックしたらアプリケーション内で遷移、Externalなリンクの場合はアプリケーション外へ遷移する...という処理になっています

また、UrlChangedonUrlChangeを通してランタイムに渡され、実際にSPAとしてのページ遷移を行う関数です

サンプルコードでは、

UrlChanged url ->

( { model | url = url }
, Cmd.none
)

となっていて、単に渡されてきたUrlModelに入れているだけです

本格的なSPAを作りたい場合、渡されてきたURLをUrl.Parserモジュールあたりの関数を使ってパースして、独自のルーティング処理を書いていくと、各ページをモジュール化してよりしっかりした構成にできると思います

そのあたりは、公式のチュートリアルUrl.Parserなんかを読んだ方が速いと思います


最後に

本当はさらに踏み込んでいろいろ書きたかったんですが、時間がないのでここまでにしておきます

多分、また別で記事を書くと思います

2019年も、Elmですよ!Elm!