VueとTSしか分からん人がElmを学習する
世界観の理解
まず読む。
ふむふむ、何となく世界観はわかった。
公式チュートリアル
公式ドキュメントのチュートリアルと日本語訳が優秀なので読んでいく。
ローカル実行環境の用意
チュートリアルではオンラインエディタも使えるんだけど、オンラインだとどこまで抽象化されているかよく分からないので、さっと環境用意できるみたいなのでやってみる。
mac のインストーラを実行して適当な場所で
elm init
を実行。
生成されたsrc/
配下に Main.elm
というファイルを作ってとりあえずこれをコピペ。
その後、
elm reactor
を実行するとなんかサーバが起動して http://localhost:8000/ で動作確認できた。
あらまあ簡単ねえ。
なんかSFCっぽい
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 に似てるっちゃ似てるので対応づけて覚えられるかも。
型
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
で似たようなことができるけど、ちょっと違うね。
型エイリアス
type alias User =
{ name : String
, age : Int
}
なんかインデントちょっと気持ち悪いけど、これが elm の慣習みたい。
TS ではこんなレコードを定義したいときは普通に型として宣言するけど、elm では型エイリアスとして定義するらしい。よく分からない。
レコードコンストラクター
Model を初期化するときに
Model "" "" ""
って記述があって、訳わからなくて草と思ってたら、レコードコンストラクターって機能だったのね。
は?カスタム型わけわからん
何これ?つら。
ぱっと見は TS の Union Type にも見えるけど、どちらかというと enum
のように機能してるっぽい。
神記事あった
ほんとにさくらちゃんはかわいいだけじゃなく優秀で素敵だわ〜。
そして、Elmにおいて 定数 とは「引数なしの関数」と同じだということを少し頭の隅に置いておいてください🤔
しれっと爆弾放りこまれたな!
よくわかんないけど定数として定義されると関数としても使えると覚えておこうぅぅぐぐぐ。
型引数
型変数とは別に型引数という概念がある?
type UserType
= GuestUser
| RegularUser
と定義すると RegularUser
は引数なしの関数(定数)として定義されるけど、
type User
= GuestUser
| RegularUser UserId
type alias UserId = String
と定義すると RegularUser
は、
RegularUser : UserId -> User
という引数をとる関数(定数)として定義されると。
うーんつまり、動的な値を扱うことができる、ちょっとリッチで型安全なEnumという理解で良さそう(雑
カスタム型完全に理解した!
Maybe
いよいよ関数型っぽくなってきた。
型レベルで値が存在しないことを表現することで、異常値がトラップできていないことにコンパイラが気づける仕組み(というか)。
たとえカスタム型でエラーを表現するのがより適切な場合であっても、初心者は特にMaybe型に興奮して、いたるところでそれを使用する傾向があります。
この辺は一般的なモデリングスキルの話だけど、公式ドキュメントでそうした問題に触れているのは素晴らしい。
TSも一応型レベルで null
や undefined
が検知できるようになってるから、アクセスしようとした時は気づくことができるけど、SFC の <template>
内で気づくのは難しいよね。
Result
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
という関数の責務としては色々やり過ぎ感が否めないんだよなあ。
まあサンプルだからこんなもんなのかな。
コマンドとサブスクリプション
httpで通信したり現在時刻を取得したり、外の世界と会話をしたい時は、コマンドという形で処理を記述してランタイムに渡して、メッセージとしてその実行結果を返却してもらうサイクルでやり取りされる。
HTTP
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)
JSON
elm/json パッケージをインストール。
elm install elm/json
初期化時、ユーザからのイベント時のコマンドを処理する方法とJSONをデコードする方法。
JSONのデコードは人間のやることじゃないので、デコーダをgenerateしてくれるライブラリを使うようにしたいなあ。
Random
elm/random パッケージをインストール。
elm install elm/random
なるほど、乱数の生成はコマンドとして外部に投げるようになってるから、乱数を扱ってても自分が書いた関数はどう足掻いても冪等になるようになってるのか。
乱数の生成器同士を合成して新しい乱数生成器を作ることもできる。
Time
elm/time パッケージをインストール。Task パッケージはインストール不要っぽい。
elm install elm/time
pub/sub パターンで時間のTickを扱う。
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every 1000 Tick
ここでようやく subscriptions
が登場。
言語レベルでこれがサポートされてるの本当にいいなあ。Vue で各コンポーネントが時間を扱う時は自前でこの pub/sub パターンを実装して頑張ってた。。
JavaScriptとの相互運用
ふむふむ、以下のようなコマンドを叩くと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 と同じ雰囲気。
フラグ
外界から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
型で受け取ってデコードするのが一般的らしい。
ポート(Ports)
外界から 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 になってしまう。
カスタムエレメンツ
カスタムエレメンツのAPIをうまく利用して、JS の機能を Elm からうまく利用する方法。
スマートだけど、ビジネスロジックが Elm から流出しないように気をつけながら使わないと途端に崩壊しちゃいそうだから乱用注意かな。