Elm 2日ほどやった感想

12月はなんとなく新しいことをやりたくなる。ということで、elm をやってみた。

大昔に触った気がするけど、文法が Haskell っぽいこと以外、何も覚えてなかった。というか当時触った signal とかがなくなってたので別物になってた。

作ったもの

勉強がてら作った、球拾いゲームみたいな何か

コードはここ https://github.com/mizchi-sandbox/elm-playground

elm-platform, svg の扱い, キー入力、乱数の副作用の分離などが学べた。乱数は面倒くさくなったので外部(JS)からSeed与える方式にして、気持ち純粋になった。

(自分の)環境構築

Parcel を使った

brew install elm # OSに応じて
mkdir elm-playground
cd elm-playground
yarn init -y
yarn add parcel-bundler node-elm-compiler

これだけ

Elm とはなにか

現時点の Elm の理解。

GUI プラットフォームの為に余分なものを削ぎ落とした関数型プログラミング言語。削ぎ落とした結果、正格評価だし、モナドもない。副作用の表現は後述する elm-platform を経由するしかない。

あくまでも GUI のための状態表現で、UI 表現とは何か?という問いから言語仕様を組み上げていってるので、汎用言語として必要なものも捨てていて、汎用プログラミングには使えない。特に後述する elm-platform で顕著。あくまで GUI 専用。

副作用: elm-platform

elm は IO モナドがないので、副作用を剥がして純粋関数に渡すには、 elm-platform を通る必要がある。redux で例えると elm-platform は redux middleware に相当する。

というわけで elm-platform で乱数を扱うっぽい処理を typescript で書くとこうなるよなー、というやつを書いてみた。

function update(model: Model, msg: Msg): [Model, Cmd | null] {
  switch (msg.msg) {
    case "roll": {
      return [model, random(newFace)];
    }
    case "newFace": {
      return [
        { face: msg.payload, history: [msg.payload, ...model.history] },
        null
      ];
    }
    default: {
      return [model, null];
    }
  }
}

全部はこちら https://gist.github.com/mizchi/bdaa2250f1707d6fb09f4115644da0aa

元ネタはここ https://package.elm-lang.org/packages/elm-lang/core/latest/Random

この関数は [model, cmd] を返す。 cmd によって副作用が発生して、次のアクションが発生する。 redux のreducer に比べると、middleware 的な処理まで一歩踏み込んでいる。

型の安心感

TypeScript の、「努力目標で型ついてたらいいよね」的な世界観と比べると、本物の静的な型の安心感が手に入り、その安心感は圧倒的。今の所ランタイムエラーは一度も発生してない。

乱数の表現の Random(Generator) や Promise 相当の Task がある。

JavaScript 世界のオブジェクトを触るときは Json.Decoder で丁寧にアクセスする必要があったり、port という外部にアクセスする専用の RPC を使い必要があり、そこはだるい。

関数型界隈特有の「サンプル書かなくても型はこうなってるから後はわかるよね?」的な世界観もややあって、 example にないものに手を出そうとすると、慣れが必要。今もまだ慣れきってはいない。

エコシステム

今回は parcel を使ったが、parcel が公式で対応してるので、めちゃくちゃ簡単だった。 import { Elm } from './Main.elm' するだけ。

公式のフォーマッタの elm-format, というかスタイルガイドはかなり癖がある。関数同士は二行空ける。特に強い意見もないので、一応従ったが、なんとなくコードが間延びした印象を受ける。

vscode で elm パッケージを入れてコードを書いている。型チェックは信頼できるが、コード補完はそこそこ程度。型チェックは通っても循環参照でコンパイルエラーが起きたことはあった。

ドキュメントが Elm 0.19 に対応していないのがよく見られる。さっきの Random のドキュメントでも Random.map2 (,) (Random.int 0 10) (Random.int 0 10) が、 Random.map2 Tuple.Pair (Random.int 0 10) (Random.int 0 10) だったり、 公式パッケージや公式ドキュメントだとしても、微妙に追いついていない。最新のドキュメントを追い続けてないと、変更についていくのは大変だろう。 0.19 がどれぐらい安定しているのか不明だが…

Reason との比較

Reason と Elm は表面的な書き心地が似ている。

Reason は一旦 ocamlコンパイルされ、表現力・制約は ocaml と同程度。ocaml が簡単に副作用や例外を発生させられてしまうのに対し、elm はその部分を外に投げることで純粋性を担保している。そのかわりに Reason で JS オブジェクトにアクセスするのが非常に簡単。泥臭いことをするなら Reason のほうがいいが、そもそも泥臭いことをするならJSでいい気がする。

学習コストに関して、 Elm は Elm で閉じた世界観だが、 Reason は ocaml と JS に精通している必要がある。reason は使い手が ocaml に強いならやっと選択肢になりうるという感じ。

使えるか

ある種のUIでは間違いなく強力だが、常に Elm が最適解ではない、という印象を受けた。Elm コミュニティもその辺割り切ってそうな印象がある。

実際に泥臭い処理をやる場合、portを駆使したり、小さいコンポーネントとして分割統治したりする必要がありそう。

おまけ: parcel と webcomponents を使った最小 Elm 入門

Elm を使うのに大仰なセットアップが必要、と思ってる人もいるかもしれないので、parcel を使った最小の例を示す。

Foo をマウントする <elm-foo> 要素を定義する。

-- elements/foo.elm
import Html exposing (div, text)
main = div [] [text "foo"]
// elements/foo.js
import { Elm } from "./Foo.elm";

customElements.define(
  "elm-foo",
  class extends HTMLElement {
    connectedCallback() {
      Elm.Main.init({ node: this });
    }
  }
);

これをマウントする

import "./elements/foo";
document.body.innerHTML = '<elm-foo></elm-foo>'

webpack でも同等の表現できると思うけど、parcel だとデフォルトでそのまま動くので楽、という感じ。