Closed22

VueとTSしか分からん人がElmを学習する

TomPenguinTomPenguin

公式チュートリアル

公式ドキュメントのチュートリアルと日本語訳が優秀なので読んでいく。
https://guide.elm-lang.jp/

TomPenguinTomPenguin

ローカル実行環境の用意

チュートリアルではオンラインエディタも使えるんだけど、オンラインだとどこまで抽象化されているかよく分からないので、さっと環境用意できるみたいなのでやってみる。

https://guide.elm-lang.jp/install/elm.html

mac のインストーラを実行して適当な場所で

elm init

を実行。

生成されたsrc/ 配下に Main.elm というファイルを作ってとりあえずこれをコピペ。

その後、

elm reactor

を実行するとなんかサーバが起動して http://localhost:8000/ で動作確認できた。
あらまあ簡単ねえ。

TomPenguinTomPenguin

なんかSFCっぽい

https://guide.elm-lang.jp/architecture/buttons.html

init, update, view 関数は独自に定義した関数で、div, button, onClick 関数は Html パッケージから import してきたやつか。

update : Msg -> Model -> Model
update msg model =
  case msg of
    Increment ->
      model + 1

    Decrement ->
      model - 1

この update 関数、Model更新しとらんやんって思ったけど、この関数の実行結果でModelが更新されるというわけか。

あとこれ、 caseじゃなくて caseだな。文は実行結果を返さないけど、式は実行結果を返すので、わざわざ return しなくても直接 case を書けると。JS にもこれ欲しいなあ〜。

view : Model -> Html Msg
view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (String.fromInt model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

この view 関数で div 関数やら button 関数の実行結果を返却すると html が生成される。イベントハンドラもここで登録すると。

全体的に Vue の SFC に似てるっちゃ似てるので対応づけて覚えられるかも。

TomPenguinTomPenguin

https://guide.elm-lang.jp/types/reading_types.html

String のリストの型は

type alias SomeList = List String

となるみたい。TS だと

type SomeList = Array<string>

に近いかな。

型注釈

アノテーションは関数の上に書けばいい。

hypotenuse : Float -> Float -> Float
hypotenuse a b =
  sqrt (a^2 + b^2)

自動的にカリー化されてるみたいで、勝手に引数が分解されるのはちょっとびっくりするかも。
アノテーションは基本書いた方がいいみたい。まあ TS でもそうだよね。

型変数

> List.length
<function>: List a -> Int

この a が型変数。小文字じゃないとだめ。TS で言うところのジェネリクスっぽい。

制約付き型変数

number, appendable, comparable などの特別な型変数を使用すると、型に制約を付けられる。TS の extends で似たようなことができるけど、ちょっと違うね。

TomPenguinTomPenguin

型エイリアス

https://guide.elm-lang.jp/types/type_aliases.html

type alias User =
  { name : String
  , age : Int
  }

なんかインデントちょっと気持ち悪いけど、これが elm の慣習みたい。

TS ではこんなレコードを定義したいときは普通に型として宣言するけど、elm では型エイリアスとして定義するらしい。よく分からない。

レコードコンストラクター

Model を初期化するときに

Model "" "" ""

って記述があって、訳わからなくて草と思ってたら、レコードコンストラクターって機能だったのね。

TomPenguinTomPenguin

は?カスタム型わけわからん

何これ?つら。

https://guide.elm-lang.jp/types/custom_types.html

ぱっと見は TS の Union Type にも見えるけど、どちらかというと enum のように機能してるっぽい。

TomPenguinTomPenguin

神記事あった

ほんとにさくらちゃんはかわいいだけじゃなく優秀で素敵だわ〜。

https://qiita.com/arowM/items/a05510b72450355b54dd

そして、Elmにおいて 定数 とは「引数なしの関数」と同じだということを少し頭の隅に置いておいてください🤔

しれっと爆弾放りこまれたな!
よくわかんないけど定数として定義されると関数としても使えると覚えておこうぅぅぐぐぐ。

TomPenguinTomPenguin

型引数

型変数とは別に型引数という概念がある?

type UserType
    = GuestUser
    | RegularUser

と定義すると RegularUser は引数なしの関数(定数)として定義されるけど、

type User
    = GuestUser
    | RegularUser UserId

type alias UserId = String

と定義すると RegularUser は、

RegularUser : UserId -> User

という引数をとる関数(定数)として定義されると。

TomPenguinTomPenguin

うーんつまり、動的な値を扱うことができる、ちょっとリッチで型安全なEnumという理解で良さそう(雑
カスタム型完全に理解した!

TomPenguinTomPenguin

Maybe

https://guide.elm-lang.jp/error_handling/maybe.html

いよいよ関数型っぽくなってきた。
型レベルで値が存在しないことを表現することで、異常値がトラップできていないことにコンパイラが気づける仕組み(というか)。

たとえカスタム型でエラーを表現するのがより適切な場合であっても、初心者は特にMaybe型に興奮して、いたるところでそれを使用する傾向があります。

この辺は一般的なモデリングスキルの話だけど、公式ドキュメントでそうした問題に触れているのは素晴らしい。

TSも一応型レベルで nullundefined が検知できるようになってるから、アクセスしようとした時は気づくことができるけど、SFC の <template> 内で気づくのは難しいよね。

TomPenguinTomPenguin

Result

https://guide.elm-lang.jp/error_handling/result.html

Resultはこういう型。

type Result error value
  = Ok value
  | Err error

で、このように使う。

isReasonableAge : String -> Result String Int
isReasonableAge input =
  case String.toInt input of
    Nothing ->
      Err "That is not a number!"

    Just age ->
      if age < 0 then
        Err "Please try again after you are born."

      else if age > 135 then
        Err "Are you some kind of turtle?"

      else
        Ok age

なるほど、確かにこれで表現力は豊かになったんだけど、isReasonableAge という関数の責務としては色々やり過ぎ感が否めないんだよなあ。
まあサンプルだからこんなもんなのかな。

TomPenguinTomPenguin

コマンドとサブスクリプション

https://guide.elm-lang.jp/effects/

httpで通信したり現在時刻を取得したり、外の世界と会話をしたい時は、コマンドという形で処理を記述してランタイムに渡して、メッセージとしてその実行結果を返却してもらうサイクルでやり取りされる。

TomPenguinTomPenguin

HTTP

https://guide.elm-lang.jp/effects/http.html

elm/http パッケージが必要なのでインストールする。

elm install elm/http

初期化時、init 関数は今までモデルの初期値を返すだけだったが、コマンドもセットで返している。

init : () -> (Model, Cmd Msg)
init _ =
  ( Loading
  , Http.get
      { url = "https://elm-lang.org/assets/public-opinion.txt"
      , expect = Http.expectString GotText
      }
  )

面白いのが、更新時もコマンドを返すことができ、エラー時に再度通信するなどかなり柔軟な処理ができるところ。

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    GotText result ->
      case result of
        Ok fullText ->
          (Success fullText, Cmd.none)

        Err _ ->
          (Failure, Cmd.none)
TomPenguinTomPenguin

JSON

https://guide.elm-lang.jp/effects/json.html

elm/json パッケージをインストール。

elm install elm/json

初期化時、ユーザからのイベント時のコマンドを処理する方法とJSONをデコードする方法。

JSONのデコードは人間のやることじゃないので、デコーダをgenerateしてくれるライブラリを使うようにしたいなあ。

TomPenguinTomPenguin

Random

https://guide.elm-lang.jp/effects/random.html

elm/random パッケージをインストール。

elm install elm/random

なるほど、乱数の生成はコマンドとして外部に投げるようになってるから、乱数を扱ってても自分が書いた関数はどう足掻いても冪等になるようになってるのか。

乱数の生成器同士を合成して新しい乱数生成器を作ることもできる。

TomPenguinTomPenguin

Time

https://guide.elm-lang.jp/effects/time.html

elm/time パッケージをインストール。Task パッケージはインストール不要っぽい。

elm install elm/time

pub/sub パターンで時間のTickを扱う。

subscriptions : Model -> Sub Msg
subscriptions model =
  Time.every 1000 Tick

ここでようやく subscriptions が登場。

言語レベルでこれがサポートされてるの本当にいいなあ。Vue で各コンポーネントが時間を扱う時は自前でこの pub/sub パターンを実装して頑張ってた。。

TomPenguinTomPenguin

JavaScriptとの相互運用

https://guide.elm-lang.jp/interop/

ふむふむ、以下のようなコマンドを叩くとJSファイルとして出力できるのか。

elm make src/Main.elm --output=main.js

出力した上で、自分で以下のようなHTMLを書くとランタイムが実行される。

<html>
<head>
  <meta charset="UTF-8">
  <title>Main</title>
  <script src="main.js"></script>
</head>

<body>
  <div id="myapp"></div>
  <script>
  var app = Elm.Main.init({
    node: document.getElementById('myapp')
  });
  </script>
</body>
</html>

この辺は Vue や React と同じ雰囲気。

TomPenguinTomPenguin

フラグ

https://guide.elm-lang.jp/interop/flags.html

外界からElmの世界にデータを注入するにはフラグという機構を使う。
例えば現在時刻を注入する場合は下記のように渡すと、

var app = Elm.Main.init({
  node: document.getElementById('myapp'),
  flags: Date.now()
});

下記のようにinit 関数の引数として受け取ることができる。

init : Int -> ( Model, Cmd Msg )
init currentTime =
  ( { currentTime = currentTime }
  , Cmd.none
  )

引数は Int としてアノテーションされているが、Elm のラインタイムがこのアノテーションと注入された値に不整合がないか検証してくれる。

注入できるデータ型は色々あるが、Json.Decode.Value 型で受け取ってデコードするのが一般的らしい。

TomPenguinTomPenguin

ポート(Ports)

https://guide.elm-lang.jp/interop/ports.html

外界から Elm の世界と動的に通信する場合はポートという I/F を使用する。

外からはこんな感じで Elm と相互に通信できる。

var app = Elm.Main.init({
    node: document.getElementById('myapp')
});

var socket = new WebSocket('wss://echo.websocket.org');

app.ports.sendMessage.subscribe(function(message) {
    socket.send(message);
});

socket.addEventListener("message", function(event) {
    app.ports.messageReceiver.send(event.data);
});

Elm 側は、port をコマンドとしてランタイムに渡すと外に情報を送信でき、subscriptions に port を登録すると外から情報を受信できる。

利用できるデータ型はフラグと同様。

ポートの I/F の設計は JS と Elm の結合点なので慎重になったほうが良さそうね。特に部分的に Elm を導入するようなユースケースの場合、その Elm アプリケーションの責務をきちんと意識しないと密結合な I/F になってしまう。

このスクラップは2023/02/17にクローズされました