Elmで作ったゲーム売上予測のフロントWebアプリ

# この資料について

これは社内勉強会の発表用資料として限定公開にしていたものを、あんまり手を加えず全公開として垂れ流しているものです。

Elm初心者ですのでコードの書き方や説明などいい加減な箇所があります、ご了承ください。

この記事で使用しているElmのバージョンは0.18です（2018/10/4時点で最新は0.19）。

# Elmとは

- フロントエンドを構築するためのDSL
  - Elmで書かれたソースがJSにコンパイルされる
- The Elm Architectureフレームワークが使える唯一の言語
- 純粋関数型言語
- 静的型付け
- Elm本体はシンプル、足りないものはライブラリで足していく感じ

--- 

# The Elm Architectureの構成

<dl>
<dt>Model</dt>
<dd>アプリケーションの状態を保持する場所</dd>
<dt>Msg</dt>
<dd>アプリケーションのイベントに対する識別子</dd>
<dt>update</dt>
<dd>Msgに応じて状態をアップデートする処理</dd>
<dt>view</dt>
<dd>状態をHTMLで表示するための箇所</dd>
</dl>

---

# Elmのサンプル

+-ボタンでカウントを行うサンプル
https://ellie-app.com/new

---

# 型宣言

## 型の定義

```elm
type alias Model =
    { count : Int }

type Msg
    = Increment
    | Decrement
```

型は先頭が大文字で定義される

## 型注釈

```elm
view : Model -> Html Msg
view model =
    ...
```

なくてもコンパイラが型推論してくれるが、あったほうがどのような関数かがわかりやすい

---

# ゲーム売上予測アプリの実装

---

## アプリの概要

- https://github.com/rhistoba/elm_game_sales_prediction
- ゲームのタイトルとジャンルを入力して売上を予測する
  - タイトルは英語のみ
  - 売上は全世界の累計枚数（100万本単位）
- 使用した技術
  - JSフレームワーク: Elm (v0.18)
  - cssフレームワーク: Semantic-UI
  - 機械学習API: Azure Machine Learning Studio + Azure API Management
     - githubにはjson-serverで偽のAPIも用意してます

### こんな感じの見た目です

<img width="464" alt="Main.png" src="https://qiita-image-store.s3.amazonaws.com/0/254436/45750197-201d-fff1-ef21-5dfbbf6c8bba.png">

---

## ディレクトリ構成(src)

```
src
├── Flags.elm
├── Main.elm
├── Messages.elm
├── Misc
│   ├── Genres.elm
│   └── Platforms.elm
├── Models.elm
├── Predictor
│   ├── Commands.elm
│   ├── Messages.elm
│   ├── Models.elm
│   ├── PredInput.elm
│   ├── PredResult.elm
│   ├── Update.elm
│   └── View.elm
├── Update.elm
├── View.elm
├── index.js
├── main.css
└── registerServiceWorker.js
```

---

## Mainモジュール

---

### Main.elm

```elm

module Main exposing (..)

import Html exposing (Html)
import Models exposing (Model, initialModel)
import Update exposing (update)
import View exposing (view)
import Flags exposing (Flags)
import Messages exposing (Msg(..))


init : Flags -> ( Model, Cmd Msg )
init flags =
    ( initialModel flags, Cmd.none )


main : Program Flags Model Msg
main =
    Html.programWithFlags
        { view = view
        , init = init
        , update = update
        , subscriptions = always Sub.none
        }
```

- Flagsは環境変数

---

### Model.elm

```elm
module Models exposing (..)

import Flags exposing (Flags)
import Predictor.Models exposing (Predictor, initialPredictor)


type alias Model =
    { flags : Flags
    , predictor : Predictor
    }


initialModel : Flags -> Model
initialModel flags =
    { flags = flags
    , predictor = initialPredictor
    }
```

- Predictor.Modelsをインポートして、Modelを入れ子構造にする

---

### Messages.elm

```elm
module Messages exposing (..)

import Predictor.Messages


type Msg
    = PredictorMsg Predictor.Messages.Msg
```

- Msgも入れ子にする

---

### Update.elm

```elm
module Update exposing (update)

import Predictor.Update
import Messages exposing (Msg(..))
import Models exposing (Model)


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PredictorMsg subMsg ->
            let
                ( updatedPredictor, cmd ) =
                    Predictor.Update.update subMsg model.predictor
            in
                ( { model | predictor = updatedPredictor }, Cmd.map PredictorMsg cmd )

```

- PredictorMsgがきたらPredictor.Updateに状態のアップデートを任せる

---

### View.elm

```elm
module View exposing (..)

import Predictor.View
import Models exposing (Model)
import Html exposing (Html)
import Messages exposing (Msg(..))


view : Model -> Html Msg
view model =
    Html.map PredictorMsg (Predictor.View.view model.flags model.predictor)
```

- ViewはPredictor.Viewをまるっと使う

---

## Predictorモジュール

---

### Predictor/Model.elm

```elm
module Predictor.Models exposing (..)

import Predictor.PredInput exposing (PredInput, initPredInput)
import Predictor.PredResult exposing (PredResult, initPredResult)


type alias Predictor =
    { pred_input : PredInput
    , pred_result : PredResult
    }


initialPredictor : Predictor
initialPredictor =
    { pred_input = initPredInput
    , pred_result = initPredResult
    }
```

- APIの入力に使う`pred_input`と、結果を受け取る`pred_result`の型を定義して使う

---

### Predictor/Messages.elm

```elm
module Predictor.Messages exposing (..)

import Http
import Flags exposing (Flags)
import Predictor.PredResult exposing (PredResult)


type Msg
    = InputTitle String
    | InputGenre String
    | InputPlatform String
    | SubmitPrediction Flags
    | ReceivePredResponce (Result Http.Error PredResult)
```

---

### Predictor/Update.elm

```elm
module Predictor.Update exposing (update)

import Predictor.Commands exposing (sendPredRequest)
import Predictor.Messages exposing (Msg(..))
import Predictor.Models exposing (Predictor)


update : Msg -> Predictor -> ( Predictor, Cmd Msg )
update msg predictor =
    case msg of
        InputTitle title ->
            let
                current_pred_input =
                    predictor.pred_input

                next_pred_input =
                    { current_pred_input | title = title }
            in
                ( { predictor | pred_input = next_pred_input }, Cmd.none )

        InputGenre genre ->
            let
                current_pred_input =
                    predictor.pred_input

                next_pred_input =
                    { current_pred_input | genre = genre }
            in
                ( { predictor | pred_input = next_pred_input }, Cmd.none )

        InputPlatform platform ->
            let
                current_pred_input =
                    predictor.pred_input

                next_pred_input =
                    { current_pred_input | platform = platform }
            in
                ( { predictor | pred_input = next_pred_input }, Cmd.none )

        SubmitPrediction flags ->
            ( predictor, sendPredRequest flags predictor.pred_input )

        ReceivePredResponce (Ok pred_result) ->
            ( { predictor | pred_result = pred_result }, Cmd.none )

        ReceivePredResponce (Err error) ->
            ( predictor, Cmd.none )

```

---

### Predictor/View.elm

```elm
module Predictor.View exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput, onSubmit, targetValue, on)
import Predictor.Models exposing (Predictor)
import Predictor.Messages exposing (Msg(..))
import Misc.Genres exposing (genres)
import Misc.Platforms exposing (platforms)
import Json.Decode as Json
import Flags exposing (Flags)


view : Flags -> Predictor -> Html Msg
view flags predictor =
    div [ class "ui container" ]
        [ h1 [] [ text "Game Sales Prediction" ]
        , p [] [ text "Input game title, genre and platform. Then predictor will give you that sales." ]
        , Html.form [ class "ui form", onSubmit (SubmitPrediction flags) ]
            [ div [ class "field" ]
                [ label [] [ text "Title" ]
                , input [ type_ "text", name "title", placeholder "Input game title here!", onInput InputTitle ] []
                ]
            , div [ class "field" ]
                [ label [] [ text "Genre" ]
                , select [ class "ui fluid dropdown", onChange InputGenre ]
                    (List.map (\genre -> option [ value genre ] [ text genre ]) genres)
                ]
            , div [ class "field" ]
                [ label [] [ text "Platform" ]
                , select [ class "ui fluid dropdown", onChange InputPlatform ]
                    (List.map (\platform -> option [ value platform ] [ text platform ]) platforms)
                ]
            , button [ class "ui button", type_ "submit" ] [ text "Submit" ]
            ]
        , h2 [] [ text ("Predicted sales: " ++ predictor.pred_result.scored_label_mean ++ "(in million)") ]
        ]


onChange : (String -> msg) -> Attribute msg
onChange handler =
    on "change" (Json.map handler targetValue)

```

---

### Predictor/Commands.elm

```elm
module Predictor.Commands exposing (predInputEncoded, predRequest, predResultDecoder, sendPredRequest)

import Flags exposing (Flags)
import Http
import Json.Decode as Decode exposing (at, field, string, index)
import Json.Decode.Pipeline exposing (custom, decode, required)
import Json.Encode as Encode
import Predictor.Messages exposing (..)
import Predictor.PredInput exposing (PredInput)
import Predictor.PredResult exposing (PredResult)


sendPredRequest : Flags -> PredInput -> Cmd Msg
sendPredRequest flags pred_input =
    predRequest flags pred_input
        |> Http.send ReceivePredResponce


predRequest : Flags -> PredInput -> Http.Request PredResult
predRequest flags pred_input =
    Http.request
        { method = "POST"
        , headers =
            [ Http.header "Content-Type" "application/json"
            , Http.header "Authorization" ("Bearer " ++ flags.gsp_api_key)
            , Http.header "Ocp-Apim-Subscription-Key" flags.gsp_subscription_key
            ]
        , url = flags.gsp_request_url
        , body = predInputEncoded pred_input |> Http.jsonBody
        , expect = Http.expectJson predResultDecoder
        , timeout = Nothing
        , withCredentials = False
        }


predInputEncoded : PredInput -> Encode.Value
predInputEncoded pred_input =
    let
        obj =
            Encode.object
                [ ( "Name", Encode.string pred_input.title )
                , ( "Genre", Encode.string pred_input.genre )
                , ( "Platform", Encode.string pred_input.platform )
                ]

        input1 =
            Encode.list [ obj ]

        inputs =
            Encode.object [ ( "input1", input1 ) ]
    in
        Encode.object [ ( "Inputs", inputs ), ( "GlobalParameters", Encode.object [] ) ]


predResultDecoder : Decode.Decoder PredResult
predResultDecoder =
    let
        customDecoder key =
            field "Results" <| field "output1" <| index 0 <| field key string
    in
        decode PredResult
            |> custom (customDecoder "Scored Label Mean")
            |> custom (customDecoder "Scored Label Standard Deviation")

```

- Commandsは外部にリクエストやレスポンスの形式を記述する場所
- Update時にリクエストが投げられる
- リクエストはエンコード、レスポンスはデコードでelmの世界を行ったりきたりする必要があるが、これの記述がめんどい

---

# 以上、ゲーム売上予測アプリの実装でした。あとはElmの長所・短所とか

---

## Elmの長所

- Elmだけ覚えればいいから、他のJSフレームワークに比べれば学習コストが少ない（はず）
  - 他のフレームワークだとRedux、Router、TypeScript、ESLintなどなど…
- 型が保証されているのでランタイムエラーが発生しない
- 標準のフォーマッターで構文エラーさえなければ自動でコードを整形してくれる

---

## Elmの短所

- メジャーなJSフレームワークに比べるとマイナーな存在
  - 採用実績が少ない
  - サードパーティー製のライブラリが貧弱
- とにかく型のつじつまを合わせてプログラミングするので、慣れてないと煩わしく感じるかも

---

## Elm関連ツール

- create-elm-app
    - プロジェクト作成ツール、webpackの設定とか自動でしてくれる
- elm-repl
    - インタプリタ
- elm-make
    - コンパイラ
- elm-test
    - テスト
    - elm-html-testもある
- elm-package
    - Elmパッケージ専用のマネージャ
    - npmやyarnとは別々に使う
- elm-format
    - コードを整形してくれるフォーマッター

---

## 個人的な感想

- ElmはJSよく知らんところから、高信頼でモダンなWebアプリを作りたい場合の最短距離になると思う
- 関数型言語を最初に触れる場合に、Elmはいい選択肢になるといろんなところで書かれているが、自分もそれほど無理なくやれたと思う