ElmでWebアプリケーション作りたい

  • 27
    Like
  • 0
    Comment
More than 1 year has passed since last update.

この記事はElm Advent Calendar 2015の20日目の記事です。

! Elm 0.16.0 (node)

はじめに

Elmを使ってWebアプリケーションを開発したいという時に役立つにかもしれないサンプルを残します。
とりあえず軽く触って試してみたいという人はオンラインエディタを使うといいです。左側にコードを書いて「Compile」をクリックすると右側に実行結果が表示されます。沢山のサンプルをつくりました、コピペするだけでも動作を確認できます。

Online Editor

基本的なコトを知りたい

変数を用いることができます。

x = 0

行コメントは--を使います。

x = 0 -- Int

このようなコードを「変数」を「束縛」すると言います。
束縛した「変数」には「型」が必要です。

x : Int
x = 0

型はいくつかあり整数の場合はIntになります。

数学の変数と同じで一度束縛した変数は再代入することはできません。

x : Int
x = 1
x = 0 -- Error!

コードが実行されたとき最初にmainという変数が呼び出され、このmainが無いとエラーが発生します。
mainの型はElement (Signal Element)またはHtml (Signal Html)のみです。
mainの型がこの2つに該当しない時点でエラーが発生するということです。

main : String
main =
    "hello,world" -- Error!

分かり易くするために、はじめのいくつかは下のように型の変換を書きました。

"hello,world" -- String

詳しくはElm Syntaxをご覧ください。

Htmlを扱いたい

install
$ elm package install evancz/elm-html

ElmにはHTML in ElmというHtmlを扱う為のモジュールがあります。

はじめにhello,worldをHtmlに出力します。

A
import Html exposing (..)

-- -- MAIN

main : Html
main =
    "hello,world" -- String
        |> text   -- String -> Html

type alias Html = Node
text : String -> Html

hello,worldと表示されたと思います。
関数textStringを受け取ってHtmlを返します。

importについて少し説明しておきます。

import Html exposing(..)
これはHtmlモジュール内の全ての関数を読み込みます。

import Html exposing(Html, text)
これはHtmlモジュールからHtmltextのみを抽出します。

import Html
これだとHtml.textのようにして参照できます。

import Html as Htm
これだとHtm.textのようにして参照できます。

B
import Html exposing (..)

-- -- MAIN

main : Html
main =
    4175            -- Number
        |> toString -- Number -> String
        |> text     -- String -> Html

toString : a -> String

String以外の場合はtoStringする必要があります。

Html.Attributesを扱いたい

文字を表示させただけではHtmlモジュールを使うほどでもないです。
Htmlタグや属性を関数を使って組み立てていきます。

C
import Html            exposing (..)
import Html.Attributes exposing (..)

main : Html
main =
    p [] [ text "hello,world" ] -- Html

p : List Attribute -> List Html -> Html

これは<p>hello,world</p>といった内容です。
class属性をつけるにはclass、スタイル属性をつけるには関数styleをつかいます。

[a, b]の型はListであり「リスト」といいます。

D
import Html            exposing (..)
import Html.Attributes exposing (..)

main : Html
main =
    p                                      -- Html
        [ class "testClass"
        , style
            [ (,) "color" "red"
            , (,) "letter-spacing" "4px" ]
        ]
        [ text "hello,world" ]             -- List Html

class : String -> Attribute
style : List (String, String) -> Attribute

赤い文字が表示されると思います。
リストが多くなるとよく分からなくなります。

(a, b)の型はTupleであり「タプル」といいます。
(,) "color" "red"("color", "red")は同じです。
(<=) = (,)とすれば"color" <= "red"とすることもできます。

E
import Html            exposing (..)
import Html.Attributes exposing (..)

main : Html
main =
    div []                      -- Html
        [ p [] [ text "hello" ] -- List Html
        , p [] [ text "world" ] -- List Html
        ]

div : List Attribute -> List Html -> Html

Htmlをネストさせるには配列にして渡します。

<div>
  <p>hello</p>
  <p>world</p>
</div>

結果としてこのようなHtmlが出力されます。

Elementを扱いたい

Htmlのモジュールに含まれる関数はList Htmlしか扱うことができません。
Elementと一緒に扱うためにHtmlElementに変換します。

F
import Graphics.Element exposing (..)
import Html             exposing (..)

-- -- MAIN

main : Element
main =
    4175                     -- Number
        |> toString          -- Number -> String
        |> text              -- String -> Html
        |> toElement 100 100 -- Html   -> Element

toElement : Int -> Int -> Html -> Element

関数toElementHtmlを受けとりElementを返します。
実際には100x100のdivが返されます。

Formを扱いたい

丸や四角といった図形などはFormという型になり、これはHtmlまたはElementに変換する必要があります。

G
import Graphics.Collage exposing (..)
import Graphics.Element exposing (..)
import Color            exposing (..)

-- -- MAIN

main : Element
main =
    [ rect 100 100                  -- Shape
        |> outlined ( dashed blue ) -- Shape     -> Form
    ]                               -- Form      -> List Form
        |> collage 200 200          -- List Form -> Element

rect : Float -> Float -> Shape
outlined : LineStyle -> Shape -> Form
dashed : Color -> LineStyle
collage : Int -> Int -> List Form -> Element

FormをリストにするとList Formになります。
関数collageはサイズと複数のFormを受け取りElementを返します。

青い四角の図形が表示されたと思います。

H
import Graphics.Collage exposing (..)
import Graphics.Element exposing (..)
import Color            exposing (..)

-- -- MAIN

main : Element
main =
    let
    sikaku =
        rect 100 100                    -- Shape
            |> outlined ( dashed blue ) -- Shape     -> Form
    in
    [ sikaku
    ]                                   -- Form      -> List Form
        |> collage 200 200              -- List Form -> Element

Elmの文法は基本的に少なく、通常のwheredoは存在しません。
letで変数に束縛し、inで値を返します。

elm packages : Graphics.Collage

Collageモジュールに含まれる関数は沢山あります。
殆どの関数がFormを返します、配列にしてcollageElementにします。

HtmlをFormに変換したい

関数collageを使えば複数のFormElement返すことができました。
FormHtmlを一緒に使う為にHtmlFormに変換してみます。

I
import Graphics.Collage exposing (Form, toForm, collage, rect, outlined, dashed)
import Graphics.Element exposing (..)
import Html             exposing (toElement, text)
import Color            exposing (..)

-- -- MAIN

main : Element
main =
    [ kazu
    ]                         -- Form      -> List Form
        |> collage 200 200    -- List Form -> Element

kazu : Form
kazu =
    4175
        |> toString           -- String
        |> text               -- String    -> Html
        |> toElement 100 100  -- Html      -> Element
        |> toForm             -- Element   -> Form

toForm : Element -> Form

mainの外でkazuに束縛しました。
関数toFromを使うことでkazuFormになります。

最後に作った二つのFormElementにします。

J
import Graphics.Collage exposing (Form, toForm, collage, rect, outlined, dashed)
import Graphics.Element exposing (..)
import Html             exposing (toElement, text)
import Color            exposing (..)

-- -- MAIN

main : Element
main =
    [ sikaku                        -- Form
    , kazu                          -- Form
    ]                               -- Form      -> List Form
        |> collage 200 200          -- List Form -> Element

sikaku : Form
sikaku =
    rect 100 100                    -- Shape
        |> outlined ( dashed blue ) -- Shape     -> Form

kazu : Form
kazu =
    4175
        |> toString                 -- String
        |> text                     -- String    -> Html
        |> toElement 100 100        -- Html      -> Element
        |> toForm                   -- Element   -> Form

青い四角の中に文字が表示されたと思います。
200x200の空間の中に100x100の青い四角、100x100の空間に左上詰めでテキストが配置されています。

FormをHtmlに変換したい

FormHtmlに変換することができます。

K
import Graphics.Collage exposing (Form, collage, rect, outlined, dashed)
import Graphics.Element exposing (..)
import Html             exposing (Html, fromElement, div, text)
import Color            exposing (..)

-- -- MAIN

main : Html
main =
    div []
        [ sikaku                   -- Html
        , kazu                     -- Html
        ]                          -- List Html

sikaku : Html
sikaku =
    [ rect 100 100                 -- Shape
        |> outlined (dashed blue)  -- Shape     -> Form
    ]
        |> collage 200 200         -- List Form -> Element
        |> fromElement             -- Element   -> Html

kazu : Html
kazu =
    4175
        |> toString                -- String
        |> text                    -- String    -> Html

fromElement : Element -> Html

Elementにした場合はHmtlElementでひとつの要素になりますが、
Htmlにした場合はElementdivになるので2つの要素になります。
これらは状況に応じて使い分けてください。

アーキテクチャを実装したい

ここまで図形とHtmlを表示することができました。
クリックといったユーザーの入力を処理しなくてはアプリケーションとは言えません。
ここからその実装に必要なアーキテクチャ(設計)について説明していきます。

ElmにはElm Architectureというアーキテクチャがあります。
内容は構成する関数を次のように分類するというものです。

model : 初期の状態を定義する

view : 状態を受け取り表示する

update : 新しい状態を返す

データの流れを一方通行にすることで、流れがとても分かり易くなります。
データの流れは2つのパターンにしかありません。

model -> view
modelが状態をviewに渡す。
viewHtmlまたはElementを表示する。

update -> view
次に「クリック」などのイベントが発生するとupdateが新しい状態をつくりviewに渡す。
viewHtmlまたはElementを再表示する。

一度に全てを考えると分からなくなるのでまずはviewから実装します。

L
import Html exposing (..)

-- -- MAIN

main : Html
main =
    view "hello,world"

-- -- VIEW

view : String -> Html
view model =
    button
        []
        [ text model ]

viewmodelという状態を受け取り表示する関数です。
view "hello,world"を書き換えると当然表示される内容も書き換わります。

次にModelを実装したいと思います。

M
import Html exposing (..)

-- -- MAIN

main : Html
main =
    view model

-- -- MODEL

type alias Model =
    { count : Int
    }

model : Model
model =
    { count = 0
    }

-- -- VIEW

view : Model -> Html
view model =
    button
        []
        [ text <| toString <| model.count ]

modelは初期の状態を定義します。
ここではcount = 0modelをつくりました。
viewmodelを受け取ってHtmlを返しています。

modelの型はModelに定義されています。
{ count = 0 }は「レコード」といいます。
レコードの型は複数の型を含むのでtype aliasを用いて新しく定義しなければいけません。

最後にupdateを定義します。

N
import Html        exposing (..)
import Html.Events exposing (..)

-- -- MAIN

main : Signal Html
main =
    Signal.map (view mailbox.address) state

-- -- SIGNAL

mailbox : Signal.Mailbox Action
mailbox =
    Signal.mailbox Init

state : Signal Model
state =
    Signal.foldp update model mailbox.signal

-- -- MODEL

type alias Model =
    { count : Int
    }

model : Model
model =
    { count = 0
    }

-- -- UPDATE

type Action
    = Init
    | Increment

update : Action -> Model -> Model
update action model =
    case action of
    Init ->
        model
    Increment ->
        { model |
          count = model.count + 1
        }

-- -- VIEW

view : Signal.Address Action -> Model -> Html
view address model =
    button
        [ onClick address Increment ]
        [ text <| toString <| model.count ]

typeは新しい型を定義します。

type Action
    = Init
    | Increment

これはInitIncrementの値をもつAction型をつくっています。

type Int
    = 0 | 1 | 2 ...

おそらくIntがこのようなことになっているのと同じです。

クリックするとmodel.count+ 1した値が返されて増えていきます。
やたら長くなりましたが、減らせる要素はレコードくらいです。

関数viewonClickaddressIncrementを受け取っています。
このaddressはどの要素で発火したかという情報、Incrementは何をするかという情報です。
関数updatemodelActionを受け取り更新し関数viewに渡します。

Signalを扱いたい

Elmにはモナドが存在しません、時間やクリックなどのような変化する情報をSignalとして処理します。
関数signal.mapは通常の関数をSignalに変換します。

例えばGraphics.Elementshowは何でも良いaを受け取る関数です。

show "hello"

Elmで副作用は実現できないとするなら「値の変わる変数」を実現するには外の世界から値を貰うしかありません。外の世界から値を貰うSignalの関数はモジュールがあります。

elm packages : Signal

O
import Mouse            exposing (..)
import Graphics.Element exposing (..)

main : Signal Element
main =
    Signal.map show Mouse.position

関数Signal.mapshowSignal型を受け取ることができるように変換しています。

Signal.foldp update model mailbox.signal

このSignal.foldpは関数updateSignalに変換し、その値の初期値をmodelにしています。

Mailboxについての記事 : miyamo_madoka / ElmのMailbox

アーキテクチャを拡張したい

modelとupdateとviewを少し変えました。

P
import Html        exposing (..)
import Html.Events exposing (..)

-- -- MAIN

main : Signal Html
main =
    Signal.map (view mailbox.address) state

-- -- SIGNAL

mailbox : Signal.Mailbox Action
mailbox =
    Signal.mailbox Init

state : Signal Model
state =
    Signal.foldp update model mailbox.signal

-- -- MODEL

type alias Model =
    { count      : Int
    , resetCount : Int
    , hover      : Bool
    }

model : Model
model =
    { count      = 0
    , resetCount = 0
    , hover      = False
    }

-- -- UPDATE

type Action
    = Init
    | Increment
    | Reset
    | Hover Bool

update : Action -> Model -> Model
update action model =
    case action of
    Init ->
        model
    Increment ->
        { model |
          count = model.count + 1
        }
    Reset ->
        { model |
          count      = 0
        , resetCount = model.resetCount + 1
        }
    Hover model' ->
        { model |
          hover = model'
        }

-- -- VIEW

view : Signal.Address Action -> Model -> Html
view address model =
    div
        []
        [ button
            [ onClick address Increment ]
            [ text <| "count : " ++ (toString <| model.count) ]
        , button
            [ onClick address Reset ]
            [ text <| "reset : " ++ (toString <| model.resetCount) ]
        , button
            [ onMouseEnter address (Hover True)
            , onMouseLeave address (Hover False)
            ]
            [ text <| (if model.hover then "on" else "off") ]
        ]

このようにしてmodelviewupdateを拡張していきます。
mainsignalの部分を書き換えることは殆どありません。
それぞれを書き換えたりしてみてください。

さいごに

需要があったら続き書きたいです。

This post is the No.20 article of Elm Advent Calendar 2015