Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Dec 30, 2022. It is now read-only.

backpaper0/osakanadm4

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

74 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OsakaNa DM 4

準備

Elmを使う準備をしましょう。

npm install -g elm elm-test elm-format

おさかな用のディレクトリを作って初期化しましょう。

mkdir osakana-elm
cd osakana-elm
elm init

reactorを起動しましょう。 ちなみにreactorというのはなんか便利なやつです。

elm reactor

http://localhost:8000/ へアクセスしてください。

なんかそれっぽい画面が表示されたらOKです。

お使いのエディタにプラグイン入れましょう

Hello, world!

やっぱりまずは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を学ぶには次のドキュメントが役立ちます。 今日も別タブで開いておいてください。

ここでは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

まずはfoodselementsへ変換してみましょう。

ヒント:List.mapList.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 -> BB -> Cがあるとします。 Elmではこの2つの関数を合成してA -> Cにできます。

関数合成は>>を使います。

elements =
    List.map (text >> List.singleton >> li []) foods

さて、次は名前に"e"を含む要素だけに絞り込んでからli要素を組み立ててみましょう。

ヒント:List.filterString.containsを使います。

また答えが見えたらアレなので行数を稼いでおきます。

(´・ω・`)
(´・ω・)
(´・ω)
(   ´・)
(     ´)
(      )
(`     )
(・`   )
(ω・`)
(・ω・`)
(´・ω・`)
(´・ω・)
(´・ω)
(   ´・)
(     ´)
(      )
(`     )
(・`   )
(ω・`)
(・ω・`)
(´・ω・`)
(´・ω・)
(´・ω)
(   ´・)
(     ´)
(      )
(`     )
(・`   )
(ω・`)
(・ω・`)
キタ━━━━━━(゚∀゚)━━━━━━ !!!!!

答え:こんな感じです。

elements =
    List.map (text >> List.singleton >> li []) (List.filter (String.contains "e") foods)

処理の順番はfilterしてからmapするのにコード上ではmapfilterよりも先に来ていますね。

これは|>を使うことで処理の順番通りに書けるようになります。

elements =
    foods |> List.filter (String.contains "e") |> List.map (text >> List.singleton >> (li []))

これでみなさんはfiltermap>>|>を知りました。 いい感じです。

あとは必要になったら調べながら進みましょう。

Browser.sandoxを使ってみよう

星マークをクリックしたら「いいね!」の件数が増えるやつを書いてみましょう。

まずは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

ここに書かれているmodelmsgは仮型変数です。

modelはアプリケーションの値、msgはイベントを表していると思ってください。

initは初期値です。

viewは値を受け取ってHtmlを返す関数です。

updatemsgmodelを受け取って新しいmodelを返す関数です。 これはReduxでいうreducer関数です。

modelとmsgを定義しよう

まずはmodelを定義してみましょう。

このアプリケーションが持つ値は「いいね!」の件数です。

次のように単にIntのエイリアスにしても良いですが、

type alias FavModel =
    Int

単一の値だけで済むアプリケーションはあまり見たことないですね。

というわけでレコードを使いましょう。

type alias FavModel =
    { count : Int }

次はmsgです。

msgはイベントのようなものです。 このアプリケーションが発火させるイベントは星マークを押したときのものです。

msgはカスタム型として定義します。

type FavMsg
    = ClickStar

カスタム型はTypeScriptでいうUnion型みたいなやつです。 Scalaだとsealedcase classを組み合わせたものっぽいですね。 あとは直和型とかバリアントって呼ばれてるやつっぽいやつです。

initviewupdateもちょろっと変更しましょう。

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 =
    model

init、view、updateの型アノテーションを書こう

Elmは割と型推論してくれますが、トップレベルに書く変数・関数ぐらいは型アノテーションを書いた方が良いです。

型アノテーションがない場合のエラーメッセージと型アノテーションがある場合のエラーメッセージだと後者の方が分かりやすいです。

init : FavModel

view : FavModel -> Html FavMsg

update : FavMsg -> FavModel -> FavModel

modelが持つ値を表示しよう

「いいね!」の件数を今はハードコーディングしています。 これを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という変数にしちゃいましょう。 星ボタンに渡しているcssattrsに変えてください。

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 "" ]
            ]

onClickmsgを受け取ってAttribute msgを返す関数です。 msgは仮型引数です。

ここでは実型引数がFavMsgです。 星ボタンをクリックしたことを表すのはClickStarなので、onClickにはClickStarを渡しています。

イベントのハンドリングはupdateで行います。 今回はClickStarイベントを受け取ってmodelcountに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リクエストを行う

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ではなくmodelCmd msgのタプルです。

viewは変わりなしですね。

updateは戻り値がmodelCmd msgのタプルになっています。

それから新しくsubscriptionsという関数を取るようになっています。

新しい要素がいくつか出てきましたね。

  • flags
  • Cmd msg
  • subscriptions
  • Sub msg

flagsはJavaScriptの世界からこんにちはできる初期値です。 ElmコードをJavaScriptへビルドしてHTMLファイルから読み込んで使おうとすると次のようなコードになります。

<div id="root"></div>
<script>
  const node = document.getElementById("root");
  const app = Elm.Main.init({ node });
</script>

このinitflagsを渡せるのです。

<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.js

Cmd msgは非同期処理や副作用を伴う処理に使われるやつです。 非同期でアレした結果や副作用をアレした結果をmsgにしてアレするとupdateが呼ばれます。

subscriptionsSub msgは今日は使わないし解説面倒なので省略します。 まあ、それを言うとflagsも使わないんですけどね、でも解説しちゃいましたね。 subscriptionsSub msgは以前ライフゲームを作った時に使ったので参考にしてみてください。

それはそうとHTTPリクエストしてみましょう。 control + creactorを止めて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

次にonClickmsgを発行させます。

MsgGetHelloを追加してonClickで発行するようにしてください。

次にupdateGetHelloをハンドリングします。

update msg model =
    case msg of
        GetHello ->
            ( model, Http.get { url = "/hello.json", expect = Http.expectString GotHello } )

Http.getCmd msgを返します。

urlexpectを指定します。 expectは期待するレスポンスの形を指定するところです。 今回はStringを期待しています。

Http.expectStringの定義をみてみましょう。

expectString : (Result Error String -> msg) -> Expect msg

Result Error Stringを受け取ってmsgを返す関数を受け取ってExpect msgを返すようになっています(このErrorHttp.Errorです)。

Result error valueは他の言語ではEitherと呼ばれるものに似ています。

Result Error Stringを受け取るGotHelloを定義しましょう。

type Msg
    = GetHello
    | GotHello (Result Http.Error String)

それにしてもonClickmsg投げたと思ったらまたGotHelloを投げるんかい、と思ったかもしれません。

ここまでの処理の流れは次の通りです。

  1. onClickGetHelloを発行する
  2. updateGetHelloを受け取ってHTTPリクエストを行う
  3. レスポンスが返ってきたらボディをStringの値にしてGotHelloを発行する
  4. updateGotHelloを受け取って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の時はエラー処理を書くべきですが、今回は省略します。

JSONデコードする

HTTPで取得したやつはJSONです。 今はそのまま表示していますが、せっかくなのでデコードしましょう。

reactorを止めてelm install elm/jsonしましょう。

JSONを受け取る場合はHttp.getexpectHttp.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 D

Json.Decodeは頻出するのでDという別名を付けています。 いやいやDて!と思ったら他の名前でも構いません。

messageというフィールドを取り出すのでJson.Decode.fieldを使います。 Json.Decode.fieldStringDecoder aを受け取ってDecoder aを返す関数です。 受け取るStringはフィールド名、Decoder aはフィールドの型です。

messageフィールドはStringなのでJson.Decode.stringを使います。

最後にDecoder stringDecoder Helloに変換するためJson.Decode.mapを使います。

というわけでDecoder Helloは次のように定義できます。

update msg model =
    let
        decoder =
            D.map Hello (D.field "message" D.string)
    in

decoderupdateの中でしか使わないのでlet inを使用しています。

Osakanagramを作る

ここまでで次のような知識を得ました。

  • レコード、カスタム型、パターンマッチ
  • Browser.elementinitviewupdatesubscriptions
  • Http
  • Json.Decode

これだけ知ったならOsakanagramを作れるはずです。

Osakanagramの参照実装をReactで作ってみました。 このリポジトリ自体がOsakanagramの参照実装です。

同じものをElmを使って実装してみましょう。

Osakanagram参照実装を起動する

まずAPIを起動しましょう。 Dockerイメージを用意しています。 あとdocker-compose.ymlも用意しているので起動は楽ちんです。

docker-compose up -d

参照実装を起動しましょう。

yarn install
yarn start

http://localhost:3000/にアクセスしてみてください。 Osakanagramが表示されましたね。

星マークをクリックしてみてください。 「いいね!」の件数が増えますよね。 無限に増やせます。 嘘です。 オーバーフローするまでは増やせます。

それではコードを書きましょう。

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •