Elmを使う準備をしましょう。
npm install -g elm elm-test elm-formatおさかな用のディレクトリを作って初期化しましょう。
mkdir osakana-elm
cd osakana-elm
elm initreactorを起動しましょう。 ちなみにreactorというのはなんか便利なやつです。
elm reactorhttp://localhost:8000/ へアクセスしてください。
なんかそれっぽい画面が表示されたらOKです。
やっぱりまずはHello, world!ですよね。
src/HelloWorld.elmを作成してエディタで開きましょう。
次のコードをコピペして保存してください。
module HelloWorld exposing (..)
import Html exposing (..)
main =
h1 [] [ text "Hello, world!" ]http://localhost:8000/src/HelloWorld.elm へアクセスしてください。
Hello, world!できましたね!
module HelloWorld exposing (..)HelloWorldというモジュールを定義して、変数や関数を全て公開する的な意味です。
JavaScriptのmodule.exports = ...とかexport default ...みたいなやつです。
import Html exposing (..)Htmlモジュールの変数や関数を全て使うぞっ、的な宣言です。
JavaScriptのrequire(...)とかimport ... from "..."みたいなやつです。
main =
h1 [] [ text "Hello, world!" ]mainという名前の変数を定義しています。
変数の本体はh1の戻り値です。
h1はHTMLのh1要素を表す関数で、引数を2つ取ります。
1つは属性のリスト、もう1つは中身のリストです。
属性のリストは[]、つまり空っぽです。
次のimportを追加して、
import Html.Attributes exposing (..)mainを次のように変えてみましょう。
main =
h1 [ style "color" "blue" ] [ text "Hello, world!" ]ブラウザをリロードすると文字が青くなったはずです。 styleはCSSが書ける関数です。
Elmを学ぶには次のドキュメントが役立ちます。 今日も別タブで開いておいてください。
ここではElmの関数を少し学びましょう。
新しくsrc/FunctionDemo.elmを作成して次のコードをコピペしてください。
module FunctionDemo exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
foods =
[ "Apple", "Banana", "Chocolate", "Donut", "Eggs Benedict" ]
elements =
[ li [] [ text "Apple" ]
, li [] [ text "Banana" ]
, li [] [ text "Chocolate" ]
, li [] [ text "Donut" ]
, li [] [ text "Eggs Benedict" ]
]
main =
ul [] elementsまずはfoodsをelementsへ変換してみましょう。
ヒント:List.mapとList.singletonを使います。
答えが見えたらアレなので行数を稼いでおきます。
(´・ω・`)
(´・ω・)
(´・ω)
( ´・)
( ´)
( )
(` )
(・` )
(ω・`)
(・ω・`)
(´・ω・`)
(´・ω・)
(´・ω)
( ´・)
( ´)
( )
(` )
(・` )
(ω・`)
(・ω・`)
(´・ω・`)
(´・ω・)
(´・ω)
( ´・)
( ´)
( )
(` )
(・` )
(ω・`)
(・ω・`)
キタ━━━━━━(゚∀゚)━━━━━━ !!!!!
答え:こんな感じです。
texts =
List.map text foods
nodes =
List.map List.singleton texts
elements =
List.map (li []) nodesこれを一行で書くと次のようになります。
elements =
List.map (li []) (List.map List.singleton (List.map text foods))ちょっと見辛いですね。
ここで2つの関数A -> BとB -> Cがあるとします。
Elmではこの2つの関数を合成してA -> Cにできます。
関数合成は>>を使います。
elements =
List.map (text >> List.singleton >> li []) foodsさて、次は名前に"e"を含む要素だけに絞り込んでからli要素を組み立ててみましょう。
ヒント:List.filterとString.containsを使います。
また答えが見えたらアレなので行数を稼いでおきます。
(´・ω・`)
(´・ω・)
(´・ω)
( ´・)
( ´)
( )
(` )
(・` )
(ω・`)
(・ω・`)
(´・ω・`)
(´・ω・)
(´・ω)
( ´・)
( ´)
( )
(` )
(・` )
(ω・`)
(・ω・`)
(´・ω・`)
(´・ω・)
(´・ω)
( ´・)
( ´)
( )
(` )
(・` )
(ω・`)
(・ω・`)
キタ━━━━━━(゚∀゚)━━━━━━ !!!!!
答え:こんな感じです。
elements =
List.map (text >> List.singleton >> li []) (List.filter (String.contains "e") foods)処理の順番はfilterしてからmapするのにコード上ではmapがfilterよりも先に来ていますね。
これは|>を使うことで処理の順番通りに書けるようになります。
elements =
foods |> List.filter (String.contains "e") |> List.map (text >> List.singleton >> (li []))これでみなさんはfilterとmap、>>と|>を知りました。
いい感じです。
あとは必要になったら調べながら進みましょう。
星マークをクリックしたら「いいね!」の件数が増えるやつを書いてみましょう。
まずはHTMLを組み立てます。
module OnClickDemo exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
main =
let
css =
[ style "cursor" "pointer"
, style "border" "0"
, style "background-color" "transparent"
, style "font-size" "large"
, style "color" "gray"
]
in
div []
[ button css [ text "☆" ]
, span [] [ text "いいね!0件" ]
]次にBrowser.sandboxを使ってみます。
とりあえず次のような感じに書いてみてください。
module OnClickDemo exposing (..)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
init =
()
view () =
let
css =
[ style "cursor" "pointer"
, style "border" "0"
, style "background-color" "transparent"
, style "font-size" "large"
, style "color" "gray"
]
in
div []
[ button css [ text "☆" ]
, span [] [ text "いいね!0件" ]
]
update () () =
()
main =
Browser.sandbox { init = init, view = view, update = update }やたら()で誤魔化していますね!
ここでBrowser.sandboxの定義を見てみましょう。
sandbox :
{ init : model
, view : model -> Html msg
, update : msg -> model -> model
}
-> Program () model msg
ここに書かれているmodelとmsgは仮型変数です。
modelはアプリケーションの値、msgはイベントを表していると思ってください。
initは初期値です。
viewは値を受け取ってHtmlを返す関数です。
updateはmsgとmodelを受け取って新しいmodelを返す関数です。
これはReduxでいうreducer関数です。
まずはmodelを定義してみましょう。
このアプリケーションが持つ値は「いいね!」の件数です。
次のように単にIntのエイリアスにしても良いですが、
type alias FavModel =
Int単一の値だけで済むアプリケーションはあまり見たことないですね。
というわけでレコードを使いましょう。
type alias FavModel =
{ count : Int }次はmsgです。
msgはイベントのようなものです。
このアプリケーションが発火させるイベントは星マークを押したときのものです。
msgはカスタム型として定義します。
type FavMsg
= ClickStarカスタム型はTypeScriptでいうUnion型みたいなやつです。
Scalaだとsealedとcase classを組み合わせたものっぽいですね。
あとは直和型とかバリアントって呼ばれてるやつっぽいやつです。
init、view、updateもちょろっと変更しましょう。
init =
FavModel 0
view model =
let
css =
[ style "cursor" "pointer"
, style "border" "0"
, style "background-color" "transparent"
, style "font-size" "large"
, style "color" "gray"
]
in
div []
[ button css [ text "☆" ]
, span [] [ text "いいね!0件" ]
]
update msg model =
modelElmは割と型推論してくれますが、トップレベルに書く変数・関数ぐらいは型アノテーションを書いた方が良いです。
型アノテーションがない場合のエラーメッセージと型アノテーションがある場合のエラーメッセージだと後者の方が分かりやすいです。
init : FavModel
view : FavModel -> Html FavMsg
update : FavMsg -> FavModel -> FavModel「いいね!」の件数を今はハードコーディングしています。
これをmodelから取るようにしましょう。
view : FavModel -> Html FavMsg
view { count } =
let
css =
[ style "cursor" "pointer"
, style "border" "0"
, style "background-color" "transparent"
, style "font-size" "large"
, style "color" "gray"
]
in
div []
[ button css [ text "☆" ]
, span [] [ text "いいね!", text <| String.fromInt count, text "件" ]
]引数はパターンマッチでレコードを分解して受け取れます。 これはJavaScriptでも似たようなことができますね。
クリックイベントを使うために次のimportを追加してください。
import Html.Events exposing (..)星ボタンの属性リストにonClickを追加します。
今、星ボタンの属性リストには変数cssが設定されています。
この変数cssにイベントハンドラを追加しても良いんですが、cssって名前にしたのでこのまま置いておきます。
ここでは新しくhandlerという変数を導入して、cssと合わせてattrsという変数にしちゃいましょう。
星ボタンに渡しているcssをattrsに変えてください。
view : FavModel -> Html FavMsg
view { count } =
let
css =
[ style "cursor" "pointer"
, style "border" "0"
, style "background-color" "transparent"
, style "font-size" "large"
, style "color" "gray"
]
handler =
onClick ClickStar
attrs =
handler :: css
in
div []
[ button attrs [ text "☆" ]
, span [] [ text "いいね!", text <| String.fromInt count, text "件" ]
]onClickはmsgを受け取ってAttribute msgを返す関数です。
msgは仮型引数です。
ここでは実型引数がFavMsgです。
星ボタンをクリックしたことを表すのはClickStarなので、onClickにはClickStarを渡しています。
イベントのハンドリングはupdateで行います。
今回はClickStarイベントを受け取ってmodelのcountに1足しましょう。
update : FavMsg -> FavModel -> FavModel
update msg model =
case msg of
ClickStar ->
{ model | count = model.count + 1 }case ofでカスタム型のパターンマッチができます。
それから新しいmodelを構築しているところに注目してください。
{ model | count = model.count + 1 }これはmodelをベースにしてcountだけを変更しています。
今はcountしかないので便利さを実感できませんが、もっとたくさんの値を持つアプリケーションを作るときは便利です。
HTTPリクエストを行うにはBrowser.sandboxではなくてBrowser.elementを使う必要があります。
定義を見てみましょう。
element :
{ init : flags -> ( model, Cmd msg )
, view : model -> Html msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
}
-> Program flags model msg
sandboxと比べると……
initは型引数flagsを取るようになっています。
また、戻り値はmodelではなくmodelとCmd msgのタプルです。
viewは変わりなしですね。
updateは戻り値がmodelとCmd msgのタプルになっています。
それから新しくsubscriptionsという関数を取るようになっています。
新しい要素がいくつか出てきましたね。
flagsCmd msgsubscriptionsSub msg
flagsはJavaScriptの世界からこんにちはできる初期値です。
ElmコードをJavaScriptへビルドしてHTMLファイルから読み込んで使おうとすると次のようなコードになります。
<div id="root"></div>
<script>
const node = document.getElementById("root");
const app = Elm.Main.init({ node });
</script>このinitにflagsを渡せるのです。
<div id="root"></div>
<script>
const node = document.getElementById("root");
const flags = "Hello, world!";
const app = Elm.Main.init({ node, flags });
</script>ちなみにElmコードをJavaScriptへビルドするには次のコマンドで行います。
elm make src/Main.elm --output main.jsCmd msgは非同期処理や副作用を伴う処理に使われるやつです。
非同期でアレした結果や副作用をアレした結果をmsgにしてアレするとupdateが呼ばれます。
subscriptionsとSub msgは今日は使わないし解説面倒なので省略します。
まあ、それを言うとflagsも使わないんですけどね、でも解説しちゃいましたね。
subscriptionsとSub msgは以前ライフゲームを作った時に使ったので参考にしてみてください。
それはそうとHTTPリクエストしてみましょう。
control + cでreactorを止めてelm install elm/httpしてから、またreactorを起動しましょう。
次の内容でhello.jsonを作ってください。
{"message":"Hello, world!"}http://localhost:8000/hello.jsonでHTTP使ってアクセスできます。 このファイルをHTTPで取ってくるコードを書いてみましょう。
まずは元となる画面を準備します。
単にメッセージとボタンが表示されているだけのものです。
Browser.elementを使用していることに注目してください。
module HttpDemo exposing (..)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type alias Model =
{ message : String }
type Msg
= Noop
init : () -> ( Model, Cmd Msg )
init () =
( Model "Initial message", Cmd.none )
view : Model -> Html Msg
view { message } =
div []
[ p [] [ text message ]
, p []
[ button [] [ text "Click me !" ]
]
]
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
main =
Browser.element { init = init, view = view, update = update, subscriptions = subscriptions }ここにHTTPリクエストの処理を足していきます。
まずimportしてください。
import Http次にonClickでmsgを発行させます。
MsgにGetHelloを追加してonClickで発行するようにしてください。
次にupdateでGetHelloをハンドリングします。
update msg model =
case msg of
GetHello ->
( model, Http.get { url = "/hello.json", expect = Http.expectString GotHello } )Http.getはCmd msgを返します。
urlとexpectを指定します。
expectは期待するレスポンスの形を指定するところです。
今回はStringを期待しています。
Http.expectStringの定義をみてみましょう。
expectString : (Result Error String -> msg) -> Expect msg
Result Error Stringを受け取ってmsgを返す関数を受け取ってExpect msgを返すようになっています(このErrorはHttp.Errorです)。
Result error valueは他の言語ではEitherと呼ばれるものに似ています。
Result Error Stringを受け取るGotHelloを定義しましょう。
type Msg
= GetHello
| GotHello (Result Http.Error String)それにしてもonClickでmsg投げたと思ったらまたGotHelloを投げるんかい、と思ったかもしれません。
ここまでの処理の流れは次の通りです。
onClickでGetHelloを発行するupdateでGetHelloを受け取ってHTTPリクエストを行う- レスポンスが返ってきたらボディを
Stringの値にしてGotHelloを発行する updateでGotHelloを受け取ってmodelを更新する
「ここまでの処理の流れ」と言ったけど最後のやつはまだ書いていませんでしたね。
というわけでGotHelloをハンドリングしましょう。
update msg model =
case msg of
GetHello ->
( model, Http.get { url = "/hello.json", expect = Http.expectString GotHello } )
GotHello (Ok message) ->
( { model | message = message }, Cmd.none )
GotHello (Err _) ->
( model, Cmd.none )Errの時はエラー処理を書くべきですが、今回は省略します。
HTTPで取得したやつはJSONです。 今はそのまま表示していますが、せっかくなのでデコードしましょう。
reactorを止めてelm install elm/jsonしましょう。
JSONを受け取る場合はHttp.getのexpectにHttp.expectStringではなくHttp.expectJsonを使います。
Http.expectJsonの定義をみてみましょう。
expectJson : (Result Error a -> msg) -> Decoder a -> Expect msg
Result Http.Error aを受け取ってmsgを返す関数とDecoder aを受け取ってExpect msgを返す関数ですね。
aは仮型変数です。
JSONをデコードして求められる任意の型ですね。
今回はmessageのみを持つレコードを定義してそれを使いましょう。
type alias Hello =
{ message : String }次はデコーダーを定義します。
まずimportしてください。
import Json.Decode as DJson.Decodeは頻出するのでDという別名を付けています。
いやいやDて!と思ったら他の名前でも構いません。
messageというフィールドを取り出すのでJson.Decode.fieldを使います。
Json.Decode.fieldはStringとDecoder aを受け取ってDecoder aを返す関数です。
受け取るStringはフィールド名、Decoder aはフィールドの型です。
messageフィールドはStringなのでJson.Decode.stringを使います。
最後にDecoder stringをDecoder Helloに変換するためJson.Decode.mapを使います。
というわけでDecoder Helloは次のように定義できます。
update msg model =
let
decoder =
D.map Hello (D.field "message" D.string)
indecoderはupdateの中でしか使わないのでlet inを使用しています。
ここまでで次のような知識を得ました。
- レコード、カスタム型、パターンマッチ
Browser.element、init、view、update、subscriptionsHttpJson.Decode
これだけ知ったならOsakanagramを作れるはずです。
Osakanagramの参照実装をReactで作ってみました。 このリポジトリ自体がOsakanagramの参照実装です。
同じものをElmを使って実装してみましょう。
まずAPIを起動しましょう。
Dockerイメージを用意しています。
あとdocker-compose.ymlも用意しているので起動は楽ちんです。
docker-compose up -d参照実装を起動しましょう。
yarn install
yarn start
http://localhost:3000/にアクセスしてみてください。 Osakanagramが表示されましたね。
星マークをクリックしてみてください。 「いいね!」の件数が増えますよね。 無限に増やせます。 嘘です。 オーバーフローするまでは増やせます。
それではコードを書きましょう。