33
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Elm のディレクトリ構成とプロジェクトのつくりかた

Last updated at Posted at 2017-12-24

この記事の内容は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 さんの記事がとても参考になりますのでぜひ。

Elmに入門してみて思ったこと

ディレクトリ構成

プロジェクトを作成する上で一番気になるのはやっぱりディレクトリ構成ですよね。

プロジェクトの進めやすさはディレクトリ構成で決まると言っても過言ではありません。(※個人の見解です)

ディレクトリ構成には大きく分けて縦割りと横割りの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では次に紹介する横割りを採用する方がいいようです。

ちなみにですけど、MsgModelの中に書いちゃってます。
MsgModelは基本セットで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があるものとないものに分けられます(これもたぶん)。

ViewMsgUpdateよりファイル数が多くなりそうです。まあ状態を持つ(つまり Model の更新によって動作などが変わる)部分と持たない部分がありますからね。

とまあ、ディレクトリ構成はこんな感じでしょうか。

モジュールの分け方

Elm チュートリアルにコンポーネントの分け方を説明しているところがありましたよね。

そこでは上位のUpdateから下位のUpdateを呼び出したり、上位のMsgの値コンストラクタに下位のMsgを持ったものを定義したりしてました。

正直これちんぷんかんぷんじゃないですか?

というわけで、これを理解するための説明を行っていきたいと思います。

完成したリポジトリはこちら。

Elmアーキテクチャ

モジュールの分け方を理解する上で、Elmアーキテクチャを理解しておくことはとても重要だと思います。

ですので、Elmアーキテクチャについてすごーく簡単に説明してみたいと思います。

  1. ユーザーはModelMsgupdateviewをそれぞれ1つだけ書く
  2. コンパイル時に以下の処理が加えられたHtml/JavaScriptコードが生成される

以下は最終的に生成されるコードの処理の流れです。

elm-architecture-flow.png

加えられた後の処理の流れとしては、

  1. RuntimeからviewModelが渡され、描画される
  2. view内ではイベント発火時にRuntimeMsgが投げられる
  3. RuntimeではMsgが投げられると、updateにそのMsgRuntimeが保持するModelが渡され実行される
  4. updateから返却されたModelによってRuntimeの保持するModelが書き換えられる
  5. 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関数を定義して生成することが多い
  • updateviewはアプリケーションで1つだけ
  • Msgはアプリケーションで1つの型のインスタンスのみ

とてもシンプルですね。
副作用を扱うことを考えると、ここにCmdSubが加わりますけど。

でもこれだと大きなものを作ろうとすると、Msgの数が増え、updateのパターンマッチも複雑になっていきます。

そこでModelMsgupdateviewを複数の型や関数、ひいてはモジュールに分けていくことを考えます。

ここで横割りの登場ですね。

サンプルプロジェクト

以下の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.userNameFieldModel.companyNameFieldに持たせておき、入力があるたびにこれらの値をinputに入力されている現在値で更新していきます。

onInputは入力があるたびに現在入力されている値を引数のMsgに渡すようですね。

そしてbuttonが押された時にメッセージAddを飛ばしてupdateを実行することで、ユーザーもしくは企業を追加しています。

UserCompany名前違うだけで中身全く同じじゃねえか」とか言ってはいけません。

サンプルですからね。

MsgとModelの分け方

まずは下位の(UsersCompaniesの)MsgModelを定義していきます。

Model/Users.elm
type Msg
    = UpdateNameField String
    | Add


type alias User =
    { name : String }


type alias Model =
    { nameField : String
    , users : List User
    }

一部省略です。
詳しくは完成版のプロジェクトを参照してください。

ユーザー管理に関係するUIの状態(nameField)とデータ(users)をまとめただけですね。

Companiesは名前違うだけなのでこれも省略です。

「じゃあなんで作ったんだ」と言われそうですけど、それは「モジュール分けないとuserNameFieldとかcompanyNameFieldとか名前被ってくるし複雑になるから分けたくならない?」みたいな話です。

そしてModelMsgはアプリケーションで1つなので、当然これらを1つにまとめる必要がありますよね。

Model.elm
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を定義します。

Update/Users.elm
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つなのでまとめていきます。

Update.elm
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.MsgCmd Model.Msgに変換しています。

このmapなんですけど、これからも色々なところで出てきますので厳密さよりイメージ重視で説明しておきますね。

ようは「ひとつ下の型に関数を適用するもの」です。

だからCmd Model.Users.MsgUsersMsg : Model.Users.Msg -> Model.MsgmapするとCmd Model.Msgになるんですね。

値コンストラクタであるUsersMsgはただの関数であることを思い出すとまあ何となく分かるんじゃないでしょうか。

あと、Elmは強い静的型付けの言語なので型を見ると理解しやすいです。

例えばCmd.mapの型はmap : (a -> msg) -> Cmd a -> Cmd msgなので、「値コンストラクタ(a -> msg)と下位のMsgをもつCmd(Cmd a)を渡すと、上位のMsgをもつCmd(Cmd msg)が返ってくるのかなあ」ってなんとなくわかりますよね。

amsgは型パラメータなので好きな型を入れられます。
まあ大体aは変換前のMsgの型、msgは変換後のMsgの型ですけどね。

そしてCompaniesMsgはほとんど同じなので以下略。

Viewの分け方

あんまり説明することがない気がするので雑めにいきます。そ〜れ〜。

View/Users.elm
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は以下略。

これもまとめていきます。

View.elm
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.MsgHtml Model.Msgに変換してますね。

型を見るとmap : (a -> msg) -> Html a -> Html msgとなっているので、「1つ目の引数の関数a -> msgを使ってHtml aHtml msgに変換できるのかなあ」ってこれもまた何となくわかりますよね。

1つめの関数っていうのはもちろん値コンストラクタですよ。

定義してきたものをRuntimeに渡す

ここまでくれば、あとは今まで定義してきたModelupdateviewRuntimeに渡すだけですね。

Main.elm
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の基本的な流れは何となく理解できたのではないでしょうか。

ModelMsgupdateviewの流れがあやふやな人はElmアーキテクチャの図を見返してくださいね。

というわけで、ここからはElmでの副作用の扱い方について説明していこうかなと思います。

Elmでの副作用の扱い方

Elmは純粋関数型言語なので副作用がありません。

ただこれはユーザーが直接副作用を扱えないというだけで、実はRuntime内で副作用は起こされてるんですよね。

だってそうじゃないと画面の描画すらできませんからね。

というわけで、ここからはElmでの副作用の扱い方を見ていきます。

主にはCmdSubを使います。
あとはJavaScriptを使えるPortsですね。

Cmd、Sub

CmdSubはどちらもすでにRuntimeに用意されている副作用をともなう処理を利用するものです。

RandomとかElmのcoreライブラリに組み込まれてますね。

CmdSubのサンプルについて今回つくるプロジェクトではCmdSubSampleという名前で実装していきます。

Cmd

CmdRuntimeに対して「Runtimeに用意されている副作用のある関数を実行して、結果の値だけくれ」と命令するものです。

処理自体はRuntimeが行うので、ユーザーが書くコードでは参照透過性が保たれるというわけです。

屁理屈みたいに聞こえましたか?
でもこれで「すべての関数は同じ入力に対して同じ出力のみを返す」っていうのが守られるんですよ。

だってRuntimeに「副作用起こして」と命令するMsgはただの値ですからね。

Cmdupdateから投げることができ、結果の値は任意のMsgとして受け取ることができます。

Model/CmdSubSample.elm
type Msg
    = Roll
    | OnResult Int


type alias Model =
    { rollResult : Int
    }
Update/CmdSubSample.elm
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.viewModel.CmdSubSample.Model.rollResultの値を表示しているだけなので省略です。

Sub

こっちは主に外部入力系の副作用を扱います。
キーボード、マウス、タイマーとかですね。

SubRuntimeに対して「この入力があったときにこのMsgを投げて」という指示を行います。

もちろんSubも参照透過性が保たれます。

Model/CmdSubSample.elm
type Msg
    = MouseMsg Position
    | KeyMsg KeyCode


type alias Model =
    { mousePosition : Position
    , downedKey : Maybe KeyCode
    }
Update/CmdSubSample.elm
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 )
Subscriptions/CmdSubSample.elm
import Model.CmdSubSample exposing (Msg(..), Model)


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.batch
        [ Mouse.clicks MouseMsg
        , Keyboard.downs KeyMsg
        ]

見たまんまですね。

マウスのクリックイベントが発火されたとき、MouseMsgが送信されます。
これはRuntimeですでに用意されている処理ですね。

Sub.batchSubの複数指定ができるみたいですね。
型はbatch : List (Sub msg) -> Sub msgですので、複数のSubを1つにまとめてるだけです。

Position型とKeyCode型についてはそれぞれMouse - mouse 1.0.1Keyboard - keyboard 1.0.1をそれぞれ参照してみてください。
まあKeyCodeChar - core 5.1.1で定義されてるのと一緒なんですけどね。

ちなみにSubSubscriptions/というディレクトリを作ります。
subscriptionsModelを受け取るので、updateと同じような感じで分ければ大丈夫ですね。

もちろん最後はまたSub.batchを使って1つにまとめます。

Subscriptions.elm
import Model exposing (Model, Msg(..))


subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ Sub.map
            CmdSubSampleMsg
            (CmdSubSample.subscriptions model.cmdSubSampleModel)
        ]

そろそろ慣れてきたんじゃないでしょうか、おなじみのmapです。

Sub Model.CmdSubSample.MsgSub Model.Msgに変換しています。
CmdSubSampleMsg : Model.CmdSubSample.Msg -> Model.Msgをそのまま使ってますね。

Sub.mapの型はmap : (a -> msg) -> Sub a -> Sub msg です。
もうそろそろ型も読めるようになってきたんじゃないですかね?(無茶振り

読めそうになかったらupdateの分け方Html.mapの説明の部分をもう一度読み返してみてください。

あとはこれをmain関数でRuntimeに渡すだけです。
まあ省略するんですけど。

Ports

CmdSubはどちらもRuntime、つまりElmのcoreライブラリですでに用意されているものを使うことが前提でした。

ではライブラリで提供されていないような副作用を扱いたいときはどうするのでしょうか。

それを解決してくれるのがPortsです。

ようはRuntimeに独自に処理を加えることができる機能ですね。

Portsのサンプルの実装はPortsSampleという名前で行っていきます。

詳細は以下の今回作成したプロジェクトを参照してください。

また、Portsについては以下の記事を参考にさせていただきました。

ElmのPortでJSを使う。 - Qiita

Elmの型とJavaScriptの型の対応表とか乗ってるので、必要になったときに参考にするといいと思います。

ElmからRuntime(JavaScript)の呼び出し

JavaScriptで書いた処理をRuntimeに組み込んで、Elmから呼び出せるようにします。

呼び出しにはCmdを使います。
coreライブラリで用意されたCmdの使い方と一つ違うのは、値が返ってこないことですね。

RandomとかのCmdは、結果をMsgで受け取れましたからね。

返ってきた値を受け取る方法は下のRuntime(JavaScript)からElmの呼び出しで説明します。

Runtimeに組み込みたいものはモジュール単位で作成します。
モジュール宣言時にport moduleと書くだけです。

またPortsは既存のModelviewupdatesubscriptionsのいずれにも属しませんので、新たにPorts/というディレクトリをつくります。

あと、Portsを使う場合はElm単体からHtml/JavaScriptへコンパイルするのではなく、JavaScriptからElmを呼び出す方法を使うことになります。

チュートリアルでWebpack使ってやったやつですね。

Ports/Console.elm
port module Ports.Console exposing (log)


port log : String -> Cmd msg
public/index.js
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 MsgMsgで結果の値を受け取ってましたけど、Portsではパラメータ多層を使ってCmd msgとします。

でもこれ、msgはどこいっちゃうんでしょうね。(誰か教えて……

使い方はcoreライブラリのCmdと一緒です。(ただしMsgは返ってこない)

まあ今回はCmdの初期値として渡してみます。

Main.elm
main : Program Never Model Msg
main =
    program
        { init = ( init, Console.log "Hellow, world!" )
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

これでプログラムが起動されたタイミングで実行されます。
詳しいタイミングは知らないですけどね。

気になる方は調べてみるといいんじゃないでしょうか。(丸投げ

Runtime(JavaScript)からElmの呼び出し

こっちはSubで利用できるタイミングを自分で実装する感じですね。

上のSubMouse.clicksなどがありましたが、これみたいなのを自分で実装してRuntimeに加えます。

こっちは先ほどのElmからRuntime(JavaScript)の呼び出しと違ってcoreライブラリのSubと完全に一緒ですね。

例としてMouse.clicksを自分で実装してみます。

Ports/MyMouse.elm
port module Ports.MyMouse exposing (Position, clicks)


type alias Position =
    { x : Int
    , y : Int
    }


port clicks : (Position -> msg) -> Sub msg
public/index.js
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として利用するだけです。

Model/PortsSamle.elm
type Msg
    = MouseMsg Position


type alias Model =
    { mousePosition : Position
    }
Update/PortsSample.elm
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 )
Subscriptions/PortsSample.elm
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.elm
subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ Sub.map
            PortsSampleMsg
            (PortsSample.subscriptions model.portsSampleModel)
        ]

Sub.mapはもう大丈夫ですね。

Subscriptions.PortsSample.subscriptionsModel.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側でごにょごにょやっちゃうんですけど、これはコードを見てもらった方が早い気がするので説明放棄です。

Ports/Greeting.elm
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に載せられて送信されるわけですね。

そんなに難しくないんじゃないかなあと思います。

そして使い方ですね。CmdSubを両方使います。

Model/PortsSample.elm
type Msg
    = UpdateNameField String
    | FetchGreetingMsg String
    | ReceiveGreetingMsg String


type alias Model =
    { nameField : String
    , greeting : String
    }
Update/PortsSample.elm
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/PortsSample.elm
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アーキテクチャの図を思い返してもらえれば読めるんじゃないかなあと思います。

viewfetchGreetingから読み始めるといいのではないでしょうか。

「なんか雑すぎない?」
はい、そうですね。

でもこれはあれですよ、みなさんの練習になるかと思ってちょっとずつ説明減らしてきただけですからね。
記事の後半になって面倒になってきたとかそういうわけでは決してないんですよ、決して。

Subscriptions.elmはさっきと同じなので省略です。

まとめ

結構長くなってしまいましたけど、このあたりで終わろうかと思います。

今回やったことは大きくは以下の3つですね。

  • ディレクトリ構成
  • モジュールの分け方
  • 副作用の扱い方

これでとりあえずはElmでプロジェクトを書き始められるのではないでしょうか。

応用は書きながら追々やっていく感じですね。

elm-test入れたり、SassとかLESSのコンパイル入れたり。

まあ次はテトリスでも書いて記事にできたらなあと思っています。

では。

33
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?