1
0

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 1 year has passed since last update.

Elmハンズオン その1

Posted at

(注)このハンズオン資料は記載時点の最新版 Elm 0.19 を対象としています。

ブラウザだけでElmをはじめる

Elm を体験するだけなら、ローカルマシンにインストールは必要ありません。

Ellieを開く ※初回アクセス時は利用規約への同意(ACCEPT TERMS)を行ってください

EllieはElmコードをコンパイルしてプレビューしてくれるオンラインサービスです。
今回のような体験にも使えますし、外部パッケージをインストールすることもできるのでちょっとした動作確認に使えます。

Ellie の画面構成

Ellieの画面構成

  • Elmのコードを記述する部分
  • HTMLのコードを記述する部分
  • プレビューを表示する部分

に分かれています。今回のハンズオンではHTMLを記述する部分は変更しませんので、HTML記述部分の右上の矢印で閉じておいても良いでしょう。

(optional) フォントを変えても良い

左上の歯車⚙アイコンで見た目などの変更ができます。フォント(サイズや種類)はご自身で使いやすいものを選択しましょう。

Ellie Settings

最初に表示されるサンプル

Ellieの初期画面の状態でコンパイルしてプレビューが表示されている状態です。

Ellieで最初に表示されるのはシンプルなカウントアップ・カウントダウンができるアプリケーションです。
初期状態は数値が 0 になっており、+1ボタンをクリックすると値が増え、-1ボタンをクリックすると値が減ります。
ボタンをポチポチ触ってみましょう。

プレビュー部分のブラウザの上部にある「RELOAD」ボタンを押してみましょう。ブラウザがリロードされる挙動を再現しており、カウントが0に戻ります。

サンプルコードをコンパイルする

サンプルコードをすこしいじってみてコンパイルしてみましょう。

まずは、 initialModel を変更し、 count = 100 にしてみます。

initialModel : Model
initialModel =
    { count = 100 }

この時点では自動的にはコンパイルされません。
「COMPILE」ボタンを押してコンパイルしてみましょう。

するとプレビューが更新されて表示される値が100になったはずです。
RELOADを押しても最初に表示される状態が 100 になっています。

Hello, world

定番ネタの 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 のままです。

add 2 view only

実際の挙動を管理しているのは 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の記述通りの状態になります。
まずは機能が動作することを確認したいので、このまま進めます。

Ellie_-_Untitled.png

このボタンは表示上追加しただけなので、ボタンを押してもリセットはされません。

「リセットする」指示を定義する

「リセットする」というアプリケーションの挙動を定義します。

まずは、アプリケーション上の指示を Msg に追加します。
この Msg は代数型と呼ばれ、 Increment, Decrement, Reset という定数のどれかになる(それ以外はありえない)ことを定義しています。

type Msg
    = Increment
    | Decrement
    | Reset

この段階でコンパイルしてみましょう。コンパイルエラーが発生するはずです。

Ellie Compile Error 1

Elm の出力するエラーは非常に丁寧です。エラーが発生した場所だけでなく、どのような修正を行うべきかまである程度教えてくれます。
今回の場合、 Msg の種類を増やしたのに、 Msg を扱っている update の方では IncrementDecrement の2パターンのみに対応する方法しか記載していないので、 Reset に対応する部分が抜けていますよ、ということを示しています。

では、 updateReset に対応させましょう。
「初期状態に戻す」ということはどのように記述すれば良いでしょうか?

すぐに思いつく方法は、他の例に倣って 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を更新する関数

だけです。

20161129180930.png

ここで、「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 ボタンがあるので開いてみましょう。

Ellie_DEBUG.png

デバッグモードでは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のドキュメント

Browser.sandbox も関数です。公式ドキュメントを見ると次のように記載されています。

sandbox :
    { init : model
    , view : model -> Html msg
    , update : msg -> model -> model
    }
    -> Program () model msg

これまでの viewupdate のような関数の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 -> ModelModel を受け取って 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 の各パターンに応じて関数を返すようになりました。
これでも正しく動作することを確認しましょう。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?