この記事の内容はElm 0.18向けのもので少し古いです。
また、Routingに関して考慮されていないのでSPA開発向けでもありません。
Elm 0.19のディレクトリ構成に関してはelm-spa-exampleが参考になります。
これは Elm Advent Calendar 2017 の 22 日目の記事です。
はじめまして、@hennin です。Qiita デビューです。
「Elm チュートリアルは一応読んだ。
で、実際どういう感じでプロジェクトつくってくの?」
今回はそういうお話です。
ちなみに今回作成したプロジェクトはこちら。
この記事を読む前に
この記事は以下の記事の内容を大いに参考にしています。
ガイドラインの題名と図だけでもいいのでさらっと見ておくといいかもです。
Nine Guidelines for Modular Elm Development – im-becoming-functional
あと、環境構築で詰まっている人は10日目の @ahuglajbclajep さんの記事がとても参考になりますのでぜひ。
ディレクトリ構成
プロジェクトを作成する上で一番気になるのはやっぱりディレクトリ構成ですよね。
プロジェクトの進めやすさはディレクトリ構成で決まると言っても過言ではありません。(※個人の見解です)
ディレクトリ構成には大きく分けて縦割りと横割りの2種類があると思うんですよね。
縦割り
Elm チュートリアルでも紹介されているやり方ですね。
root/
├ Foo/
│ ├ Model.elm
│ ├ Update.elm
│ └ View.elm
│
├ Bar/
│ ├ Model.elm
│ ├ Subscriptions.elm
│ ├ Update.elm
│ └ View.elm
│
├ Main.elm
├ Model.elm
├ Subscriptions.elm
├ Update.elm
└ View.elm
コンポーネントで分ける感じです。
実装がコンポーネントごとにまとまってるので分かりやすいですね。
ただこれ、作ってみると分かりますけどコンポーネント単位で再利用することってあんまりないんですよね。
まあ実際のプロジェクトでは汎用コンポーネントより具体的なコンポーネントを作ることの方が多くなるのは当たり前です。
それに、コンポーネント単位で再利用するよりModel
とかview
の単位で再利用する方がお手軽ですよね。
というわけで、シンプルを目指すElmでは次に紹介する横割りを採用する方がいいようです。
ちなみにですけど、Msg
はModel
の中に書いちゃってます。
Msg
とModel
は基本セットでimport
されるので、その方が色々と楽なんですよね。
横割り
どこでもわりとよく見かける分け方ですね。
root/
├ Model/
│ ├ Foo.elm
│ ├ Bar.elm
│ ├ Hoge.elm
│ └ Fuga.elm
│
├ Subscriptions/
│ └ Bar.elm
│
├ Update/
│ ├ Foo.elm
│ └ Bar.elm
│
├ View/
│ ├ Header.elm
│ ├ Foo.elm
│ └ Bar.elm
│
├ Main.elm
├ Model.elm
├ Subscriptions.elm
├ Update.elm
└ View.elm
種類ごとに分ける感じです。
利点としては、それぞれのディレクトリ内で使いまわせる関数や型を作るのが簡単なことが挙げられますね。
Model
内全体で使うモデルとか、View
内の色々なところで呼び出すビューの関数とか。
書いてる感じModel
のファイル数が結構多くなりますね。
Msg
をもつModel
のファイル数とUpdate
のファイル数はまあ一致します。というか一致させるものですよねこれ(たぶん)。
ちなみにModel
は対応するMsg
があるものとないものに分けられます(これもたぶん)。
View
もMsg
やUpdate
よりファイル数が多くなりそうです。まあ状態を持つ(つまり Model の更新によって動作などが変わる)部分と持たない部分がありますからね。
とまあ、ディレクトリ構成はこんな感じでしょうか。
モジュールの分け方
Elm チュートリアルにコンポーネントの分け方を説明しているところがありましたよね。
そこでは上位のUpdate
から下位のUpdate
を呼び出したり、上位のMsg
の値コンストラクタに下位のMsg
を持ったものを定義したりしてました。
正直これちんぷんかんぷんじゃないですか?
というわけで、これを理解するための説明を行っていきたいと思います。
完成したリポジトリはこちら。
Elmアーキテクチャ
モジュールの分け方を理解する上で、Elmアーキテクチャを理解しておくことはとても重要だと思います。
ですので、Elmアーキテクチャについてすごーく簡単に説明してみたいと思います。
- ユーザーは
Model
、Msg
、update
、view
をそれぞれ1つだけ書く - コンパイル時に以下の処理が加えられたHtml/JavaScriptコードが生成される
以下は最終的に生成されるコードの処理の流れです。
加えられた後の処理の流れとしては、
-
Runtime
からview
にModel
が渡され、描画される -
view
内ではイベント発火時にRuntime
にMsg
が投げられる -
Runtime
ではMsg
が投げられると、update
にそのMsg
とRuntime
が保持するModel
が渡され実行される -
update
から返却されたModel
によってRuntime
の保持するModel
が書き換えられる - 1.に戻る
という感じになります。
Runtime
については、Elmのコンパイル時に加えられる隠蔽された処理の塊みたいなものと思ってください。
Cmd
とここには登場していないSub
については後のElmでの副作用の扱い方で説明しますね。
ここで覚えておいてほしいのは、以下の4つのことです。
- Elm アーキテクチャは以下の 2 つの型と 2 つの関数からなる
Model
Msg
update : Msg -> Model -> (Model, Cmd Msg)
view : Model -> Html Msg
-
Model
はアプリケーションでインスタンスが1つのみ、init
関数を定義して生成することが多い -
update
とview
はアプリケーションで1つだけ -
Msg
はアプリケーションで1つの型のインスタンスのみ
とてもシンプルですね。
副作用を扱うことを考えると、ここにCmd
とSub
が加わりますけど。
でもこれだと大きなものを作ろうとすると、Msg
の数が増え、update
のパターンマッチも複雑になっていきます。
そこでModel
、Msg
、update
、view
を複数の型や関数、ひいてはモジュールに分けていくことを考えます。
ここで横割りの登場ですね。
サンプルプロジェクト
以下の1ファイルのプロジェクトをモジュールに分けていきたいと思います。
ユーザーと企業を管理する簡単なアプリケーションです。
まあ管理といっても追加しか機能がありませんけど。
import Html exposing (program, div, Html, text, input, button)
import Html.Attributes exposing (value)
import Html.Events exposing (onInput, onClick)
type Msg
= UpdateUserNameField String
| AddUser
| UpdateCompanyNameField String
| AddCompany
type alias User =
{ name : String }
type alias Company =
{ name : String }
type alias Model =
{ userNameField : String
, companyNameField : String
, users : List User
, companies : List Company
}
init : ( Model, Cmd Msg )
init =
( { userNameField = ""
, companyNameField = ""
, users = []
, companies = []
}
, Cmd.none
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateUserNameField str ->
( { model | userNameField = str }, Cmd.none )
AddUser ->
( { model
| users = { name = model.userNameField } :: model.users
, userNameField = ""
}
, Cmd.none
)
UpdateCompanyNameField str ->
( { model | companyNameField = str }, Cmd.none )
AddCompany ->
( { model
| companies = { name = model.companyNameField } :: model.companies
, companyNameField = ""
}
, Cmd.none
)
userView : User -> Html Msg
userView user =
div []
[ text "User: "
, text user.name
]
companyView : Company -> Html Msg
companyView company =
div []
[ text "Company: "
, text company.name
]
view : Model -> Html Msg
view model =
div []
[ div []
[ input [ value model.userNameField, onInput UpdateUserNameField ] []
, button [ onClick AddUser ] [ text "Add" ]
]
, div []
(model.users |> List.map userView)
, div []
[ input [ value model.companyNameField, onInput UpdateCompanyNameField ] []
, button [ onClick AddCompany ] [ text "Add" ]
]
, div []
(model.companies |> List.map companyView)
]
main : Program Never Model Msg
main =
program
{ init = init
, view = view
, update = update
, subscriptions = (\_ -> Sub.none)
}
input
の入力値はModel.userNameField
とModel.companyNameField
に持たせておき、入力があるたびにこれらの値をinput
に入力されている現在値で更新していきます。
onInput
は入力があるたびに現在入力されている値を引数のMsg
に渡すようですね。
そしてbutton
が押された時にメッセージAdd
を飛ばしてupdate
を実行することで、ユーザーもしくは企業を追加しています。
「User
とCompany
名前違うだけで中身全く同じじゃねえか」とか言ってはいけません。
サンプルですからね。
MsgとModelの分け方
まずは下位の(Users
とCompanies
の)Msg
とModel
を定義していきます。
type Msg
= UpdateNameField String
| Add
type alias User =
{ name : String }
type alias Model =
{ nameField : String
, users : List User
}
一部省略です。
詳しくは完成版のプロジェクトを参照してください。
ユーザー管理に関係するUIの状態(nameField
)とデータ(users
)をまとめただけですね。
Companies
は名前違うだけなのでこれも省略です。
「じゃあなんで作ったんだ」と言われそうですけど、それは「モジュール分けないとuserNameField
とかcompanyNameField
とか名前被ってくるし複雑になるから分けたくならない?」みたいな話です。
そしてModel
とMsg
はアプリケーションで1つなので、当然これらを1つにまとめる必要がありますよね。
type Msg
= UsersMsg Users.Msg
| CompaniesMsg Companies.Msg
type alias Model =
{ usersModel : Users.Model
, companiesModel : Companies.Model
}
Model
は見たまんまです。
Msg
は、下位のMsg
を引数にもつ値コンストラクタを定義することで、下位のMsg
も合わせて1つのMsg
のインスタンスってことにしちゃうんですね。
この引数に渡された下位のMsg
は次のupdateの分け方でパターンマッチを使って取りだします。
ちなみにですけどimport
は
import Model.Users as Users
import Model.Companies as Companies
みたいにしてます。
以下のupdate
でもview
でも大体こんな感じですね。
updateの分け方
まずはUsers
用のupdate
を定義します。
import Model.Users as Users exposing (Msg(..), Model)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateNameField str ->
( { model | nameField = str }, Cmd.none )
Add ->
( { model
| users = { name = model.nameField } :: model.users
, nameField = ""
}
, Cmd.none
)
こちらも一部省略です。
以下でも特に断らない限りは重量なところだけ抜粋したコードを載せていきます。
Model.Users.Model
のためだけのupdate
なのでとてもシンプルになりましたね。
例によってModel.Companies.Model
用のupdate
は省略。
そしてupdate
もアプリケーションに1つなのでまとめていきます。
import Model exposing (Msg(..), Model)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UsersMsg subMsg ->
let
( updatedUsersModel, usersCmd ) =
Users.update subMsg model.usersModel
in
( { model | usersModel = updatedUsersModel }
, Cmd.map UsersMsg usersCmd
)
CompaniesMsg subMsg ->
let
( updatedCompaniesModel, companiesCmd ) =
Companies.update subMsg model.companiesModel
in
( { model | companiesModel = updatedCompaniesModel }
, Cmd.map CompaniesMsg companiesCmd
)
簡単に説明していきますね。
まずパターンマッチでUsersMsg
からModel.Users.Msg
のインスタンスをsubMsg
として取り出します。
そして下位のupdate
であるUpdate.Users.update
を使って、更新後のModel.Users.Model
であるupdatedUserModel
と、更新後のCmd Model.Users.Msg
であるusersCmd
を手に入れます。
Update.Users.update
の型はupdate : Model.Users.Msg -> Model.Users.Model -> (Model.Users.Model, Cmd Model.Users.Msg)
ですからね。
あとはその結果を(Model.Model, Cmd Model.Msg)
に変換して返すだけですね。
その変換を行っているのがHtml.map
で、Cmd Model.Users.Msg
をCmd Model.Msg
に変換しています。
このmap
なんですけど、これからも色々なところで出てきますので厳密さよりイメージ重視で説明しておきますね。
ようは「ひとつ下の型に関数を適用するもの」です。
だからCmd Model.Users.Msg
にUsersMsg : Model.Users.Msg -> Model.Msg
をmap
するとCmd Model.Msg
になるんですね。
値コンストラクタであるUsersMsg
はただの関数であることを思い出すとまあ何となく分かるんじゃないでしょうか。
あと、Elmは強い静的型付けの言語なので型を見ると理解しやすいです。
例えばCmd.map
の型はmap : (a -> msg) -> Cmd a -> Cmd msg
なので、「値コンストラクタ(a -> msg
)と下位のMsg
をもつCmd
(Cmd a
)を渡すと、上位のMsg
をもつCmd
(Cmd msg
)が返ってくるのかなあ」ってなんとなくわかりますよね。
a
とmsg
は型パラメータなので好きな型を入れられます。
まあ大体a
は変換前のMsg
の型、msg
は変換後のMsg
の型ですけどね。
そしてCompaniesMsg
はほとんど同じなので以下略。
Viewの分け方
あんまり説明することがない気がするので雑めにいきます。そ〜れ〜。
import Model.Users exposing (Msg(..), Model, User)
userView : User -> Html Msg
userView user =
div []
[ text "User: "
, text user.name
]
view : Model -> Html Msg
view model =
div []
[ div []
[ input
[ value model.nameField
, onInput UpdateNameField
]
[]
, button [ onClick Add ] [ text "Add" ]
]
, div []
(model.users |> List.map userView)
]
Companies
は以下略。
これもまとめていきます。
import Model exposing (Msg(..), Model)
view : Model -> Html Msg
view model =
div []
[ Html.map UsersMsg (Users.view model.usersModel)
, Html.map CompaniesMsg (Companies.view model.companiesModel)
]
さっそく出てきました、Html.map
です。
ここではmap
は、Users.view model.usersModel
の返り値がHtml Model.Users.Msg
なのでHtml Model.Users.Msg
をHtml Model.Msg
に変換してますね。
型を見るとmap : (a -> msg) -> Html a -> Html msg
となっているので、「1つ目の引数の関数a -> msg
を使ってHtml a
をHtml msg
に変換できるのかなあ」ってこれもまた何となくわかりますよね。
1つめの関数っていうのはもちろん値コンストラクタですよ。
定義してきたものをRuntime
に渡す
ここまでくれば、あとは今まで定義してきたModel
、update
、view
をRuntime
に渡すだけですね。
main : Program Never Model Msg
main =
program
{ init = ( init, Cmd.none )
, view = view
, update = update
, subscriptions = subscriptions
}
init
は各Model
の中でそのModel
の初期値を返す関数として定義して、例によってModel.elm
で1つにまとめたやつです。
想像がつかないという人は完成版のプロジェクトを見てください。
subscriptions
については後のSubで説明します。
あとはコンパイルすればRuntime
の処理を加えたHtml/JavaScriptコードが生成されますね。
ここまでくればElmの基本的な流れは何となく理解できたのではないでしょうか。
Model
、Msg
、update
、view
の流れがあやふやな人はElmアーキテクチャの図を見返してくださいね。
というわけで、ここからはElmでの副作用の扱い方について説明していこうかなと思います。
Elmでの副作用の扱い方
Elmは純粋関数型言語なので副作用がありません。
ただこれはユーザーが直接副作用を扱えないというだけで、実はRuntime
内で副作用は起こされてるんですよね。
だってそうじゃないと画面の描画すらできませんからね。
というわけで、ここからはElmでの副作用の扱い方を見ていきます。
主にはCmd
、Sub
を使います。
あとはJavaScript
を使えるPorts
ですね。
Cmd、Sub
Cmd
とSub
はどちらもすでにRuntime
に用意されている副作用をともなう処理を利用するものです。
Random
とかElmのcoreライブラリに組み込まれてますね。
Cmd
、Sub
のサンプルについて今回つくるプロジェクトではCmdSubSample
という名前で実装していきます。
Cmd
Cmd
はRuntime
に対して「Runtime
に用意されている副作用のある関数を実行して、結果の値だけくれ」と命令するものです。
処理自体はRuntime
が行うので、ユーザーが書くコードでは参照透過性が保たれるというわけです。
屁理屈みたいに聞こえましたか?
でもこれで「すべての関数は同じ入力に対して同じ出力のみを返す」っていうのが守られるんですよ。
だってRuntime
に「副作用起こして」と命令するMsg
はただの値ですからね。
Cmd
はupdate
から投げることができ、結果の値は任意のMsg
として受け取ることができます。
type Msg
= Roll
| OnResult Int
type alias Model =
{ rollResult : Int
}
import Model.CmdSubSample exposing (Msg(..), Model)
import Random
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Roll ->
( model, Random.generate OnResult (Random.int 1 6) )
OnResult res ->
( { model | rollResult = res }, Cmd.none )
サイコロを振る例ですね。
重要なところだけ抜粋です。
Random.generate
の型はgenerate : (a -> msg) -> Generator a -> Cmd msg
となっています。
つまり「Msg
の値コンストラクタとGenerator a
を受け取って、Cmd msg
を返す」関数となります。
噛み砕いて説明しちゃうと、Generator a
で「どんな副作用を起こしてどんな結果(a
)を得るか」を指定して、a -> msg
で「その結果(a
)をどのMsg
として受け取るか」を決めてるんですね。
View.CmdSubSample.view
はModel.CmdSubSample.Model.rollResult
の値を表示しているだけなので省略です。
Sub
こっちは主に外部入力系の副作用を扱います。
キーボード、マウス、タイマーとかですね。
Sub
はRuntime
に対して「この入力があったときにこのMsg
を投げて」という指示を行います。
もちろんSub
も参照透過性が保たれます。
type Msg
= MouseMsg Position
| KeyMsg KeyCode
type alias Model =
{ mousePosition : Position
, downedKey : Maybe KeyCode
}
import Model.CmdSubSample exposing (Msg(..), Model)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
MouseMsg position ->
( { model | mousePosition = position }, Cmd.none )
KeyMsg keyCode ->
( { model | downedKey = Just keyCode }, Cmd.none )
import Model.CmdSubSample exposing (Msg(..), Model)
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.batch
[ Mouse.clicks MouseMsg
, Keyboard.downs KeyMsg
]
見たまんまですね。
マウスのクリックイベントが発火されたとき、MouseMsg
が送信されます。
これはRuntime
ですでに用意されている処理ですね。
Sub.batch
でSub
の複数指定ができるみたいですね。
型はbatch : List (Sub msg) -> Sub msg
ですので、複数のSub
を1つにまとめてるだけです。
Position
型とKeyCode
型についてはそれぞれMouse - mouse 1.0.1、Keyboard - keyboard 1.0.1をそれぞれ参照してみてください。
まあKeyCode
はChar - core 5.1.1で定義されてるのと一緒なんですけどね。
ちなみにSub
もSubscriptions/
というディレクトリを作ります。
subscriptions
はModel
を受け取るので、update
と同じような感じで分ければ大丈夫ですね。
もちろん最後はまたSub.batch
を使って1つにまとめます。
import Model exposing (Model, Msg(..))
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Sub.map
CmdSubSampleMsg
(CmdSubSample.subscriptions model.cmdSubSampleModel)
]
そろそろ慣れてきたんじゃないでしょうか、おなじみのmap
です。
Sub Model.CmdSubSample.Msg
をSub Model.Msg
に変換しています。
CmdSubSampleMsg : Model.CmdSubSample.Msg -> Model.Msg
をそのまま使ってますね。
Sub.map
の型はmap : (a -> msg) -> Sub a -> Sub msg
です。
もうそろそろ型も読めるようになってきたんじゃないですかね?(無茶振り
読めそうになかったらupdateの分け方のHtml.map
の説明の部分をもう一度読み返してみてください。
あとはこれをmain
関数でRuntime
に渡すだけです。
まあ省略するんですけど。
Ports
Cmd
とSub
はどちらもRuntime
、つまりElmのcoreライブラリですでに用意されているものを使うことが前提でした。
ではライブラリで提供されていないような副作用を扱いたいときはどうするのでしょうか。
それを解決してくれるのがPorts
です。
ようはRuntime
に独自に処理を加えることができる機能ですね。
Ports
のサンプルの実装はPortsSample
という名前で行っていきます。
詳細は以下の今回作成したプロジェクトを参照してください。
また、Ports
については以下の記事を参考にさせていただきました。
Elmの型とJavaScriptの型の対応表とか乗ってるので、必要になったときに参考にするといいと思います。
ElmからRuntime(JavaScript)の呼び出し
JavaScriptで書いた処理をRuntime
に組み込んで、Elmから呼び出せるようにします。
呼び出しにはCmd
を使います。
coreライブラリで用意されたCmd
の使い方と一つ違うのは、値が返ってこないことですね。
Random
とかのCmd
は、結果をMsg
で受け取れましたからね。
返ってきた値を受け取る方法は下のRuntime(JavaScript)からElmの呼び出しで説明します。
Runtime
に組み込みたいものはモジュール単位で作成します。
モジュール宣言時にport module
と書くだけです。
またPorts
は既存のModel
、view
、update
、subscriptions
のいずれにも属しませんので、新たにPorts/
というディレクトリをつくります。
あと、Ports
を使う場合はElm単体からHtml/JavaScriptへコンパイルするのではなく、JavaScriptからElmを呼び出す方法を使うことになります。
チュートリアルでWebpack使ってやったやつですね。
port module Ports.Console exposing (log)
port log : String -> Cmd msg
const Elm = require("../src/Main.elm");
const mountNode = document.getElementById("root");
const app = Elm.Main.embed(mountNode);
app.ports.log.subscribe(function(str) {
console.log(str);
});
Ports
では中身の実装をJavaScript
に任せるため、関数の型宣言だけを行います。
あと型宣言にはport
を付けます。
port module
内には普通の関数も書けるので、差別化のためですね。
subscribe
は「購読する」という意味で、プログラミングでは何かの合図を待ち受けて処理を実行するときによく使われます。
ここではElmからの関数の呼び出しを合図に引数の関数を実行するという意味になりますね。
coreライブラリのCmd
ではCmd Msg
のMsg
で結果の値を受け取ってましたけど、Portsではパラメータ多層を使ってCmd msg
とします。
でもこれ、msg
はどこいっちゃうんでしょうね。(誰か教えて……
使い方はcoreライブラリのCmd
と一緒です。(ただしMsg
は返ってこない)
まあ今回はCmd
の初期値として渡してみます。
main : Program Never Model Msg
main =
program
{ init = ( init, Console.log "Hellow, world!" )
, view = view
, update = update
, subscriptions = subscriptions
}
これでプログラムが起動されたタイミングで実行されます。
詳しいタイミングは知らないですけどね。
気になる方は調べてみるといいんじゃないでしょうか。(丸投げ
Runtime(JavaScript)からElmの呼び出し
こっちはSub
で利用できるタイミングを自分で実装する感じですね。
上のSubでMouse.clicks
などがありましたが、これみたいなのを自分で実装してRuntime
に加えます。
こっちは先ほどのElmからRuntime(JavaScript)の呼び出しと違ってcoreライブラリのSub
と完全に一緒ですね。
例としてMouse.clicks
を自分で実装してみます。
port module Ports.MyMouse exposing (Position, clicks)
type alias Position =
{ x : Int
, y : Int
}
port clicks : (Position -> msg) -> Sub msg
const Elm = require("../src/Main.elm");
const mountNode = document.getElementById("root");
const app = Elm.Main.embed(mountNode);
document.body.addEventListener("click", function(e) {
app.ports.clicks.send({ x: e.clientX, y: e.clientY });
});
そんなに難しいところはないですね。
あとはSub
として利用するだけです。
type Msg
= MouseMsg Position
type alias Model =
{ mousePosition : Position
}
import Model.PortsSample exposing (Msg(..), Model)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
MouseMsg position ->
( { model | mousePosition = position }, Cmd.none )
import Model.PortsSample as PortsSample exposing (Msg(..), Model)
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.batch
[ MyMouse.clicks MouseMsg
]
クリックされたときModel.mousePosition
が更新されます。
view
は省略してますけどModel.mousePositions
を表示させてるだけですね。
そしてSubscriptions
も例によって例のごとく1つにまとめてしまいます。
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Sub.map
PortsSampleMsg
(PortsSample.subscriptions model.portsSampleModel)
]
Sub.map
はもう大丈夫ですね。
Subscriptions.PortsSample.subscriptions
はModel.PortsSample.Msg
を返しますが、これをModel.Msg
を返すように変換します。
型はmap : (a -> msg) -> Sub a -> Sub msg
でしたね。
もう読めますよね。
PortsのCmdっぽい使い方
Runtime(JavaScript)からElmの呼び出しはcoreライブラリのSub
と同じでしたけど、なぜかElmからRuntime(JavaScript)の呼び出しの方はcoreライブラリのCmd
と違ってそれ単体では結果をMsg
で受け取ることができませんでしたよね。
ではどうやって結果の値を受け取るのかというと、これらを両方使います。
JavaScript
側でごにょごにょやっちゃうんですけど、これはコードを見てもらった方が早い気がするので説明放棄です。
port module Ports.Greeting exposing (fetchGreeting, receiveGreeting)
port fetchGreeting : String -> Cmd msg
port receiveGreeting : (String -> msg) -> Sub msg
const Elm = require("../src/Main.elm");
const mountNode = document.getElementById("root");
const app = Elm.Main.embed(mountNode);
app.ports.fetchGreeting.subscribe(function(name) {
app.ports.receiveGreeting.send("Hello, " + name + "!");
});
こんな感じです。
fetchGreeting
でElmからRuntime
(JavaScript)のsubscribe
のコールバック関数を実行。
コールバック関数はElmから飛んできたMsg
の中身を引数として受け取ってますね。
で、そのコールバック関数の中でElmのreceiveGreeting
を実行。
入力された名前に挨拶する文字列がElm側に送信され、下のsubscriptions
で設定されたMsg
に載せられて送信されるわけですね。
そんなに難しくないんじゃないかなあと思います。
そして使い方ですね。Cmd
とSub
を両方使います。
type Msg
= UpdateNameField String
| FetchGreetingMsg String
| ReceiveGreetingMsg String
type alias Model =
{ nameField : String
, greeting : String
}
import Model.PortsSample exposing (Msg(..), Model)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateNameField str ->
( { model | nameField = str }, Cmd.none )
FetchGreetingMsg name ->
( model, Greeting.fetchGreeting name )
ReceiveGreetingMsg greeting ->
( { model | greeting = greeting }, Cmd.none )
view : Model -> Html Msg
view { mousePosition, nameField, greeting } =
div []
[ mousePositionView mousePosition
, greetingView nameField greeting
]
greetingView : String -> String -> Html Msg
greetingView nameField greeting =
div []
[ label []
[ text "name: " ]
, input
[ value nameField
, onInput UpdateNameField
]
[]
, button
[ onClick (FetchGreetingMsg nameField) ]
[ text "FetchGreeting" ]
, div []
[ text greeting ]
]
はい、コード読めば分かりますね。(暴言)
まあ実際Elmアーキテクチャの図を思い返してもらえれば読めるんじゃないかなあと思います。
view
のfetchGreeting
から読み始めるといいのではないでしょうか。
「なんか雑すぎない?」
はい、そうですね。
でもこれはあれですよ、みなさんの練習になるかと思ってちょっとずつ説明減らしてきただけですからね。
記事の後半になって面倒になってきたとかそういうわけでは決してないんですよ、決して。
Subscriptions.elm
はさっきと同じなので省略です。
まとめ
結構長くなってしまいましたけど、このあたりで終わろうかと思います。
今回やったことは大きくは以下の3つですね。
- ディレクトリ構成
- モジュールの分け方
- 副作用の扱い方
これでとりあえずはElmでプロジェクトを書き始められるのではないでしょうか。
応用は書きながら追々やっていく感じですね。
elm-test入れたり、SassとかLESSのコンパイル入れたり。
まあ次はテトリスでも書いて記事にできたらなあと思っています。
では。