インタラクティブなトイアプリを作る必要があったので
electronとFRPが前提のHaskell風味AltJSであるelmを使って作ってみた。
さらにNativeなAPIを使う方法についても調査し、
今回調査した知見をもとに
elmtrnというボイラープレートを作った
Quickstart
git clone git@github.com:yasuyuky/elmtrn.git
cd elmtrn
npm install
elm make -y
gulp
上記のようにすれば公式のclockサンプルを
謎の時計ウィジェットにしたものが立ち上がる。
elm + electron
elmで書いたものをelectronで動かすのは想像していたより簡単で
electronのhello worldに含まれるような
以下のような標準的なapp.js
で指定してるhtmlの部分を
elmで生成されるhtmlに置き換えるだけであっさり動いた。
var app = require('app');
var BrowserWindow = require('browser-window');
var mainWindow = null;
app.on('window-all-closed', function() {
app.quit();
});
app.on('ready', function() {
mainWindow = new BrowserWindow({width: 800, height: 600});
mainWindow.loadUrl('file://' + __dirname + '/app.html');
mainWindow.on('closed', function() {
mainWindow = null;
});
});
上記のように基本的にはメインプロセスはjsで
あとはレンダラープロセスで描画するhtmlを
elmでコンパイルしたものにするというのが簡単だと思う。
メインプロセスのjsもelmで書きたいという向きもあるかもしれないが、
簡単なSPAっぽいアプリを書く分にはメインに関してはほぼ定型文で
BrowserWindowに渡すパラメータを調整するくらいだと思うので
今のところ個人的にはこれで満足している
elmのビルド
elmは仮想DOMをアレコレするみたいなので
elmでSPAを作る場合は基本的には全部elmで完結させて開発するのがいいのかなと思う。
(とゆうか他のフレームワークと混ぜるとか出来なさそう)
それも踏まえた上で自動ビルドとlivereloadをしたかったので
ぼくのかんがえたさいきょうのElectronを参考にしてgulpfileを書いた。
elmで状態を扱う際のアーキテクチャ
公式のサンプルの時計を動かしたりする分には必要無いが
現実世界のアプリケーションを書くためには当然状態を扱うことになる。
そうすると途端にFRPに特有のアーキテクチャのパターンで実装することになる。
公式でも紹介されているToDoアプリが非常に参考になる。
-
model
と呼ばれる状態を定義するデータ型 action
- Signalにそって
model
を更新するupdate
-
model
からアウトプットを生成するview
をそれぞれ分けて定義し、
信号の流れを一方通行にするデザインパターンが有効で、
上記アプリもそのパターンで書かれている。
これはReact周辺ではfluxと呼ばれるアーキテクチャになるのだと思う。
自分の理解では以下の感じの対応になる
elm | flux |
---|---|
actions | Action Creators |
update | Dispatcher |
Model | Store |
view | React Views |
viewに関してはelmもReact.js同様にVirtualDOMでよろしくやってくれるはず。
ちなみにelmtrnではこのパターンにそって公式のclockサンプルを
fluxライクなスタイルに書き直してある。
StartApp
上記アーキテクチャのボイラープレートをまとめてくれたstart-appというライブラリがある。
これを使うことで自動的に上記のアーキテクチャに沿った設計が出来る。
しかし、本当に薄いラッパーでしかないので、
その部分を自分で書いても大した負担にはならないし、
後述のような任意のマウスイベントや時間変化のシグナルを扱う
場合に想定から外れてしまうので最終的には自分は使わなかった。
自分が定義した以外のSignalも扱うには?
Signal
をfoldp
してる場所に扱いたいSignalを
マージするのがいいっぽい。
model = Signal.foldp update initialModel actions.signal
↓
model = Signal.foldp update initialModel signals
signals = Signal.mergeMany [ actions.signal
, UpdateTime <~ every second
]
type Action = NoOp
| UpdateTime Time
<~
(Signal.map) で Signal Time から Signal Actionに変換している
Signalを扱う場所を一箇所に集めるのはアーキテクチャとして
正しいというのもあるけど、そもそも上記以外で
(例えば一番最初のサンプルのようにmainのなかで)
Signalを扱おうと思うとエラーを吐いて動かしたアプリがハングしたりしたので
素直にデザインパターンに従うのがいいかと思う。
do記法の不在
Haskellを使った事がある人がelmの文法を眺めるとdo
記法で
逐次的に処理を記述するような記法がないことに気付くかもしれない。
これはおそらく意図的な設計だと思う。
実際にfluxのようなアーキテクチャを採用すると、逐次的な処理というのは
actionからの一方通行のSignalのループを回す事によって実現し、
do記法による逐次処理のようなものはそのルールを壊す事になるので相性がよくない。
do記法がないので必然的にモナド則に沿ってインスタンスを作っても
そこまでうれしさがないし、リスト内包記法みたいなものも排除されている。
列挙が必要なものは引数としてリストをとるようになっている。
elmでNativeモジュールを通してElectronのAPIを使う
ところでelectronにはデスクトップアプリとしての機能を実現するAPI群がある。
これをElmから使うにはどうしたらいいかという問題があるが、
ElmのNative moduleを書くを参考にラッパーを書くことで使えるようになる。
レンダラープロセスでつかうAPIのうち、例えばClipboardを扱うためのAPIの
ラッパーは以下のようになる。
var clipboard = require('clipboard');
Elm.Native.Clipboard = {};
Elm.Native.Clipboard.make = function(elm) {
elm.Native = elm.Native || {};
elm.Native.Clipboard = elm.Native.Clipboard || {};
if (elm.Native.Clipboard.values) return elm.Native.Clipboard.values;
function readText(_) {
return clipboard.readText();
}
function writeText(s) {
clipboard.writeText(s);
return s;
}
return elm.Native.Clipboard.values = { // Export
readText: readText,
writeText: writeText
};
};
注意したい点として、上記readTextは環境からクリップボードを読み込むものなので
本来的には引数が必要ないはずだが、Nativeモジュールの制約で引数をとるようになっている
使う際はNative.Clipboard.readText ()
のようにユニット(空のタプル)を渡すのがいいかと思う。
所感など
elmよくできてる。HaskellっぽいけどFRPを前提に言語設計されてて
モナドとかも出てこないので気軽にHaskell風味シンタックスに慣れるにもいいのでは。
拡張もわりと素直に書ける。
FRPのアーキテクチャというか設計パターンは一度覚えてしまうと
GUI設計は全部これでやりたくなる。
個人的にはphpにおけるhtmlや様々な言語における生SQLなどのように
言語内に他の言語のシンタックスが入り込むのがあまり好きでないし
その点でReact.jsはJSXを前提としててあまり好きでなかった(あくまで個人の感想です)。
elmはelm-htmlのライブラリなどを使えばviewの部分の記述を
言語として違和感のシンタックスで書けていい。
少なくともそんなに大きなものでないならHaskell由来の
簡潔なシンタックスでコンパクトにかける印象がある。
elm-htmlについてももう少し書こうかと思ったけど力尽きた…