- 環境:
- Elm 0.18
- nodejs 6.11.4
- Elmについての入門は以下:
- Docs portal
- Official Guide
- チュートリアル(日本語版あり)
- 自分でもそのうちまとめたい。。
1. Webアプリ以外もElmで書ける(EXPERIMENTAL)
Elmのいいところはバランスの良いところで、Haskellの持つ文法的・言語機能的にcrypticな部分を設計思想として意図的に削ぎ落としつつも1、純粋関数型・静的型付け言語の利点を存分に享受できる点だと思っています。コンパイラは非常に開発者に優しく(!)、リファクタリングもデバッグも安心してできます。安心感大事。
そんな言語なので、隙あらば使いたくなるわけです。ちなみに筆者は軟派なのでHaskellは眺めるだけのことが多いですが、Elmに機能的に満足できくなった人はHaskellに行ったりPureScriptに行ったりする選択肢があるのでそこそこ潰しも効くと思います。Elmをいっぱい書いていると自然と型クラスが何故嬉しいのかわかるようになる(欲しくなる)し、何故か雰囲気でモナドもわかるようになるかもしれません。
モナドの概念、まず Elm で1年くらい |> andThen を書き続けてストレスが溜まった頃に「はい」って差し出すとすっと入ると思う。
— Yosuke Torii / ジンジャー (@jinjor) October 15, 2017
公式でもreliableなWebアプリがdelightfulに開発できる言語と謳っているので、ElmはWebアプリを書くためのDSLなのか? などという議論もあったりしますが、もちろんそれだけってことはなくて、汎用言語としてなんでも好きなプログラムをElmで書けます。
ただし、ブラウザ以外の場所で動くプログラムを作ろうと思うと、若干nodejsの助けを多めに受ける必要があり、Elmで書いたプログラムはnodejsプログラムの中で「エンジン」として働かせるようなイメージになります。この記事ではその部分のやり方をまとめます。
1.1. Webアプリ以外もElmで書く「べき?」
べき、ではないです。適材適所という言葉があります。
実験的にElmでサーバ・クライアント両方書いているeeue56/take-homeのREADMEから引用すると、
As an Elm programmer, I like to write Elm! As a server-side Elm programmer, I hate writing yet another integration library that wraps around a Node library that uses mutable objects and callbacks in a weird way.
Elmは今のところWebアプリを開発する言語として活発に使われ始めているところで、そちらの方面でのコア開発やpackage開発は様々に行われていますが、サーバサイドの開発で必要になるような機能はないことのほうが多いです。ちょっとしたことをやろうと思うだけでも思わぬ苦労をすることがあるでしょう。Nodeライブラリへのport
を書くのもすんなりとはいかないことも多いようです。
追記: また、EvanもOn “General-Purpose” LanguagesでElmの現状の方向性をはっきりと表明しています。(言語コア開発やアカデミックに関わるトピックをまとめているelm-lang/projectsレポジトリの内容なので、エンドユーザ向けの提言というわけではありませんが)
I also think it is important to clearly inform people what the language is decent at right now. That way they do not waste their time on paths we already know that Python or Erlang or C can do better right now.
ということで、あくまでElmの強みは「今は」Webアプリ開発です。そしてそこにおいて非常に強力だと思います。
でも他の分野への適用も、趣味・実験の範囲でやってみてもいいと思います。筆者はやろうとしています。
(Evanのメッセージにならって、項目名にEXPERIMENTALとでっかく書いておきました)
2. Elmでワーカープログラム
ちょうど今、AmazonのProduct Advertising APIをクロールしてデータを集め、Kindle本の推薦を行うようなアプリを作ろうかと思っており、せっかくなのでElmでサーバサイドをどの程度書けるか試してみる、というのが動機です。
クローラは要はサーバ上でずっと動いているワーカー的なプログラムになるわけで、よくあるElmでWebアプリを作るチュートリアルとは少し毛色が違います。しかしElmは0.18からPlatform.program
というAPIをcoreの中に持っています。これを使うと、viewを持たない"headless"プログラムを書くことができ、外部のnodeプログラムからmoduleを参照して使うことができるのです。
ここで面白いのが、headlessだからといってWebアプリのElmプログラムと変わらず、ほとんど同じようにTEAを使って書ける点です。外部コードとの対話をport
で実現する点も同じです。むしろport
を初めて使うきっかけにheadlessプログラムを書いてみるのはありかもしれません。
2.1. 準備
まず最低限:
$ npm i elm -D
Elmはグローバルにインストールしたものを使ってもいいのですが、ここではローカルインストールします。
package.jsonにnpm scriptsを書いておき、node_modules/.bin/以下に配置されるバイナリにnpm run
経由でアクセスできるようにしておくのが最近よく見る手法ですね(要出典)
{
...
"scripts": {
"build": "elm-make --yes --debug --warn --output=dist/worker.js src/Worker.elm",
"elm-package": "elm-package"
},
"devDependencies": {
"elm": "^0.18.0"
}
}
Headlessプログラムであればビルドには上記のようにelm-make
を生で使ってもいいです。その場合、Elmプログラムと我らがV8との間を繋ぐもう一つのnodeプログラム(エントリポイントとなるもの)を用意し、生成物(dist/worker.js)を呼び出して使うことになります。が、どっちにしろ複数ファイル間に依存関係が生じるのが確定しているのであれば、最初からWebpackとelm-webpack-loaderを使っておくほうがいいと思います。その場合以下のようになります。
$ npm i webpack elm-webpack-loader -D
{
...
"scripts": {
"build": "webpack",
"clean": "rm -rf dist/",
"elm-package": "elm-package"
},
"devDependencies": {
"elm": "^0.18.0",
"elm-webpack-loader": "^4.3.1",
"webpack": "^3.6.0"
}
}
const path = require('path')
module.exports = {
entry: {
worker: ['./src/worker.js'],
},
target: 'node',
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
},
module: {
rules: [
{
test: /\.elm$/,
exclude: [
/elm-stuff/,
/node_modules/,
],
use: {
loader: 'elm-webpack-loader',
options: {
warn: true,
},
},
},
],
}
}
ElmチュートリアルでWebpackで開発する場合の流れが丁寧に解説されていますが、ここではさらにそれを削り込んでNodeプログラムの中で動かす場合の最小の構成にしました。見て分かる通りチュートリアルのwebpack.config.jsより小さくまとまるので、Elmへの導入としてこれもありではないかと思います。
target: 'node'
はWebpackに、対象プログラムをブラウザではなくNode環境向けにビルドするよう指示するプロパティで、こうするとライブラリによってはNode環境専用の変数やAPIが読み込まれます(aws-sdk
など)。今回は不要ですが一応書いておきます。
以下Webpackを使った環境を前提とします。生成物のdist/worker.jsは、それ自体がエントリポイントとなりつつ、Elm由来のプログラムも含みます。
2.2. Hello World! (sort of)
早速Elmを書きます。
module Worker exposing (main)
import Platform
type alias Model =
{}
type Msg
= NoOp
init : ( Model, Cmd Msg )
init =
( {}, Cmd.none )
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
main : Platform.Program Never Model Msg
main =
Platform.program
{ init = init
, update = update
, subscriptions = subscriptions
}
(Elmのシンタックスハイライトがねえ。```haskell
使ってます)
見ての通り何もしないプログラムですが、ポイントはPlatform.program
を使っているところです。引数recordの中にview
が不要ですね。
そしてこれを呼び出すグルーコードを用意します。
var Elm = require('./Worker.elm')
var worker = Elm.Worker.worker()
module名Worker
にしたのは失敗だったけど気にしない。Html.program
の場合はElm.<ModuleName>.fullscreen()
やElm.<ModuleName>.embed()
していましたが、headlessプログラムの場合はworker()
を呼びます。
ビルド・実行してみます。
$ npm run build
Hash: 754d4db0c118b4785ca3
Version: webpack 3.8.0
Time: 891ms
Asset Size Chunks Chunk Names
worker.js 72.3 kB 0 [emitted] worker
[0] multi ./src/worker.js 28 bytes {0} [built]
[1] ./src/worker.js 68 bytes {0} [built]
[2] ./src/Worker.elm 69.3 kB {0} [built]
$ node dist/worker.js
$ echo $?
0
実行しても何も出ません。当然ですね。
が、ここでもう一つポイントがあって、このプログラムは実行後すぐ返ってきます。つまりworker()
というそれっぽい関数を呼んだからといってElmで書いたプログラム部分がそれだけで何らかの待受ループを開始して常駐するということはありません。今のままでは本当に何もしないmoduleがそこに存在しているというだけです。
2.3. Hello World! (really)
つまりWorker
モジュールはエンジンですから、点火する必要があるわけです。
このプログラムのエントリポイントはあくまでworker.js(ビルド前も、ビルド後も)ですから、そこからElmのコードに命令を送り込むことになります。JavaScriptとElmとの間で対話するには? port
ですね。ということでport
を書きましょう。
@@ -1,4 +1,4 @@
-module Worker exposing (main)
+port module Worker exposing (main)
import Platform
@@ -8,7 +8,7 @@ type alias Model =
type Msg
- = NoOp
+ = StartMsg String
init : ( Model, Cmd Msg )
@@ -19,13 +19,18 @@ init =
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
- NoOp ->
- ( model, Cmd.none )
+ StartMsg text ->
+ text
+ |> Debug.log "Started"
+ |> always ( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
- Sub.none
+ start StartMsg
+
+
+port start : (String -> msg) -> Sub msg
main : Platform.Program Never Model Msg
@@ -1,3 +1,5 @@
var Elm = require('./Worker.elm')
var worker = Elm.Worker.worker()
+
+worker.ports.start.send('Hello World!')
port
についてはofficial guideのこちらに導入があります。
こちらも参考: ElmのPortでJSを使う。
ポイントとしては、ここでsubscriptions
を使うことになります。外部環境から何らかの入力を得て、それをmessageとして受け取る仕組みで、Elmプログラムの中で重要な役割を担います。
ビルドして実行してみましょう。
$ node dist/worker.js
/Users/yumatsuzawa/workspace/blaze/dist/worker.js:3271
var _ymtszw$blaze$Worker$start = _elm_lang$core$Native_Platform.incomingPort('start', _elm_lang$core$Json_Decode$string);
^
ReferenceError: _elm_lang$core$Json_Decode$string is not defined
at Object.<anonymous> (/Users/yumatsuzawa/workspace/blaze/dist/worker.js:3271:87)
at Object.<anonymous> (/Users/yumatsuzawa/workspace/blaze/dist/worker.js:3317:4)
at __webpack_require__ (/Users/yumatsuzawa/workspace/blaze/dist/worker.js:20:30)
at Object.<anonymous> (/Users/yumatsuzawa/workspace/blaze/dist/worker.js:77:11)
at __webpack_require__ (/Users/yumatsuzawa/workspace/blaze/dist/worker.js:20:30)
at Object.<anonymous> (/Users/yumatsuzawa/workspace/blaze/dist/worker.js:70:18)
at __webpack_require__ (/Users/yumatsuzawa/workspace/blaze/dist/worker.js:20:30)
at /Users/yumatsuzawa/workspace/blaze/dist/worker.js:63:18
at Object.<anonymous> (/Users/yumatsuzawa/workspace/blaze/dist/worker.js:66:10)
at Module._compile (module.js:570:32)
大ショック!!! ランタイムエラーです。
Elmではランタイムエラーは起こらないんじゃなかったのかよ! 裏切られた気持ちになるのも仕方ないですね。Googleさんだってこう言ってる:
(それっぽい候補が釣れるような単語を探したけど)
まあ落ち着きます。port
はJavaScriptとの界面ですからね。こういうところではエラーが起きるのも仕方ない。
調査するとこのような Issueが見つかります。どうもmodule依存関係の解決のバグのようだ。じゃあJavaScript関係ねえじゃん! Fixは0.19に向けてすでに入っているとのこと。ここではワークアラウンドします。
@@ -1,6 +1,7 @@
port module Worker exposing (main)
import Platform
+import Json.Decode
type alias Model =
使ってないようにみえるけれど、port処理の中身でJavaScriptの値をElmの値に変換するために使われているJson.Decode
をimport
しておきます。ビルドして実行すると、
$ node dist/worker.js
Started: "Hello World!"
$ echo $?
0
無事Elmプログラムが呼ばれていることを確認できました。しかし、すぐ返ってくるのはそのままですね。
2.4. ループさせる
クローラのようなプログラムでは、適当な間隔で無限に(interruptされるまで)処理を繰り返す事になるでしょう。つまりループさせたい。
JavaScriptでナイーブにこれを実現しようとするなら、setTimeout
で関数を再帰的に呼ぶとか、scheduler系のライブラリを使うとかでしょうか。
一方Elmでは、このような時間依存のコードはsubscriptionを使って書けます。
@@ -2,19 +2,21 @@ port module Worker exposing (main)
import Platform
import Json.Decode
+import Time exposing (Time)
type alias Model =
- {}
+ { running : Bool }
type Msg
= StartMsg String
+ | TickMsg Time
init : ( Model, Cmd Msg )
init =
- ( {}, Cmd.none )
+ ( { running = False }, Cmd.none )
update : Msg -> Model -> ( Model, Cmd Msg )
@@ -23,12 +25,20 @@ update msg model =
StartMsg text ->
text
|> Debug.log "Started"
+ |> always ( { model | running = True }, Cmd.none )
+
+ TickMsg time ->
+ time
+ |> Debug.log "Tick"
|> always ( model, Cmd.none )
subscriptions : Model -> Sub Msg
-subscriptions model =
- start StartMsg
+subscriptions { running } =
+ if running then
+ Time.every (5 * Time.second) TickMsg
+ else
+ start StartMsg
port start : (String -> msg) -> Sub msg
少し工夫して、modelの中にループが実行中かどうか示すフラグ(running
)を導入し、実行中であればTime.every
で経過時間イベントに対してsubscribeし、実行中でなければstart
portにsubscribeしておくようにしています。
modelの状態に応じてsubscribeするイベントを絞り込むことで、アプリケーションが内部的に管理する状態を減らすことができます。2
これをビルドして実行すると、
$ node dist/worker.js
Started: "Hello World!"
Tick: 1508440727195
Tick: 1508440732201
Tick: 1508440737201
Tick: 1508440742203
Tick: 1508440747206
Tick: 1508440752206
Tick: 1508440757209
Tick: 1508440762215
Tick: 1508440767221
Tick: 1508440772223
Tick: 1508440777229
^C⏎
無事、ループするプログラムを作れました。あとはそれぞれのtickごとに好きな処理をさせればいいということで、ワーカープログラムを作る土台ができました。
実際の環境で実行する際のプロセスの死活監視などについては、Foreverやpm2など、一般的なツールを使えばいいと思います。スケールアウトできるようなプログラムであれば、pm2で複数インスタンスをコア数分だけparallelに走らせるのもいいでしょう。
2.5. (おまけ) Graceful Stop
今のままだとプログラムはCtrl + Cで強制停止させることになります。それ自体は問題なくても、Tickごとの処理が一通り終わるのを待ってgracefulに止めたいかもしれません。
色々なやり方がありそうですが、シンプルにworker.jsの中でSIGINT
にハンドラを仕込んでみます。
@@ -3,3 +3,7 @@ var Elm = require('./Worker.elm')
var worker = Elm.Worker.worker()
worker.ports.start.send('Hello World!')
+
+process.on('SIGINT', () => {
+ worker.ports.interrupt.send(null)
+})
interrupt
portからElmに伝達しています。せっかくElmで書くからには、可能な限り処理をすぐに移譲して、あとはElmで書きたいですね。Elm側も終了処理のコードを書きます。
@@ -7,17 +7,19 @@ import Time exposing (Time)
type alias Model =
{ running : Bool
+ , terminating : Bool
}
type Msg
= StartMsg String
+ | InterruptMsg ()
| TickMsg Time
init : ( Model, Cmd Msg )
init =
- ( { running = False }, Cmd.none )
+ ( { running = False, terminating = False }, Cmd.none )
update : Msg -> Model -> ( Model, Cmd Msg )
@@ -26,25 +28,36 @@ update msg model =
StartMsg text ->
text
|> Debug.log "Started"
- |> always ( { model | running = True }, Cmd.none )
+ |> always ( { model | running = not model.terminating }, Cmd.none )
TickMsg time ->
time
|> Debug.log "Tick"
|> always ( model, Cmd.none )
+ InterruptMsg () ->
+ Debug.log "Interrupted" "Terminating now!"
+ |> always ( { model | running = False, terminating = False }, Cmd.none )
+
subscriptions : Model -> Sub Msg
subscriptions { running } =
- if running then
- Time.every (5 * Time.second) TickMsg
- else
- start StartMsg
+ Sub.batch
+ [ interrupt InterruptMsg
+ , (if running then
+ Time.every (5 * Time.second) TickMsg
+ else
+ start StartMsg
+ )
+ ]
port start : (String -> msg) -> Sub msg
+port interrupt : (() -> msg) -> Sub msg
+
+
main : Platform.Program Never Model Msg
main =
Platform.program
少々こまっしゃくれたコードになりました。
根幹としては、interrupt
portからのトリガーをInterruptMsg
で受け、running
をFalse
にしています。これによってsubscriptionの状態が変わり、Time.every
がなくなりますので、アプリケーション内に生きているタイマーがなくなり、プログラム全体が停止します。3
ただし、SIGINTの受け入れはアプリケーションのライフサイクルの中でいかなるタイミングでも行いたいという注文があるかと思います。そこでterminating
という新たな状態を導入して、たとえstart
がトリガーされてもterminating = True
であればrunning = True
に遷移しないようロックしています。(running = not model.terminating
)
その上で、interrupt
portのsubscribeは常に行うようにし、Sub.batch
を使ってその他のsubscriptionとまとめています。
実行して、割り込んでみましょう。
$ node dist/worker.js
Started: "Hello World!"
Tick: 1508449990436
Tick: 1508449995442
^CInterrupted: "Terminating now!"
$ echo $?
0
Time.every
を剥がすだけで、安全にプログラムを停止させられました。少しいじってみるとわかりますが、Time.every
を剥がさなければもちろん停止しません。SIGINTをトラップしてしまっているので、その場合Ctrl+Cでは止められないプログラムの出来上がりです。SIGKILLしましょう。
実際のプログラムでは、真にgraceful stopするためにはもう少し細かいチェックをしたりするかもしれません。とはいえ、必要な状態変数をmodelに適宜導入しつつ、TEAの枠組みの中でこのように書いていけるでしょう。
3. まとめ
ElmでWebアプリ以外もまあまあ書けそうです。
追記: とはいえ、今はデッカく"EXPERIMENTAL"が付くことは忘れずに。
記事内のコードは https://github.com/ymtszw/blaze に置いてあります。
文中で言及した、余暇開発中のKindle本レコメンダアプリのレポジトリなので、他のコードがどんどん増えていくかと思いますが、Worker.elmとworker.jsはキープしておく予定です。他にもネタがあったら書く予定。
-
「
.
とか見ても初心者何もわからんだろ! でも>>
ならなんとなく記号の向き的に処理の流れがわかるんじゃなーい?(前方関数合成)」とか。$
の代わりに<|
(後方関数適用)とか。 ↩ -
ただ、外部のJavaScript環境から任意のタイミングで呼ばれる可能性のあるport関数のsubscriptionを外してしまうと、当然ながらその後その関数に対して値が
send
された場合何も起こりません。(とはいえ、幸いRuntime errorにはならないようです。ありがとうElm!!!!) ↩ -
ちなみに、特にJavaScriptから値を受け取る必要がないケースでは、Elm側は
()
つまりUnit型を使い、JavaScriptからはnull
を送ることができます。ここでは大きな意味はありません。 ↩