コールバックを要求するJavaScript APIをElmから使う際の実装方法について

この記事は [Elm Advent Calendar 2015](http://qiita.com/advent-calendar/2015/elm)の14日目の記事として書かれました。

# tl;dr

Elmからコールバックを要求するJavaScript APIモジュールの実装方法について検討し、
そのProof of Conceptとして[WebSocketのモジュールを作った](https://github.com/yasuyuky/elm-websocket)。

ついでにElectronのipcRenderer APIを叩く際の例も記述した。

# 目標

世に数多あるコールバックを要求するJavascript APIを
ElmのNative moduleでラップしてElmの世界で自由自在に使いたい。

## 対象読者

前提知識として基本的なElm Architectureについて理解してる人が対象読者です。
Nativeモジュールの書き方を理解していると尚いいです。

## 対象バージョン
```
Elm Platform 0.16.0
```

## 免責

ここで紹介している方法は[elm-lang/core](https://github.com/elm-lang/core)
のソースを読み解いたりしながら独自研究した方法なので
将来においてずっと使えるかはわかりません。

## コールバックを要求するJavaScriptAPIの例

### NativeなWebSocket

```ws.js
var ws = new WebSocket("ws://localhost:8000/");

ws.onopen = function() {
	...
};

ws.onmessage = function(e) {
	...
};

ws.onerror = function(e) {
	...
};

ws.onclose = function() {
	...
};
```


# 問題

いつものように Native moduleを書いていこうとすると、
**どうやってJSのコールバックにElmの処理を渡せばいいのか？**という疑問に当たる。

例えばElmの関数をJSの世界で実行する関数は実は[runtimeの中に用意されてる。](https://github.com/elm-lang/core/blob/c685cc0fb104dbf69f8cb82bda40d15caddd1c17/src/Native/Runtime.js#L588-L635)

これを使うとなると、

1. 初期化時にコールバックで実行するためのElmの関数を渡し、
2. それを`A2()`など使ってラップしたJSの関数をコールバックとして渡す

というような手順を踏む事になると思うが、以下のような幾つかの問題がある

- 初期化時に様々なコールバックとそれに食べさせる為のJSON Decoder(後述)を喰わせる事になり、引数が多くなり使いづらいモジュール（関数）になってしまう
- コールバックに渡された値を再びElmの世界に持ってこれない

そこで、今回はこれから記述するようにコールバック関数にElmの処理を渡し、
**Signalを通知させる方法で実質的なコールバックを実現する。**

# コールバックで渡される値をSignalで通知する

以下、今回作成した[WebSocket用モジュール](https://github.com/yasuyuky/elm-websocket)を例に見ていくが、
ソースコードはそんなに長くないのでNativeモジュール書いた事のある人は
見るだけで理解できるかもしれない。

以下にポイントを挙げる。

## Native.Signalの準備

ElmのNative moduleにはSignalを作る為のJSの関数が用意されており、
`Native`の`Signal`モジュールを`make`する事により使用可能になる。

```src/Native/WebSocket.js
Elm.Native.WebSocket = {};
Elm.Native.WebSocket.make = function(localRuntime) {
  localRuntime.Native = localRuntime.Native || {};
  localRuntime.Native.WebSocket = localRuntime.Native.WebSocket || {};
  if (localRuntime.Native.WebSocket.values) 
  　　return localRuntime.Native.WebSocket.values;

  var NS = Elm.Native.Signal.make(localRuntime);
  var Utils = Elm.Native.Utils.make(localRuntime);
```

ここで`var NS = Elm.Native.Signal.make(localRuntime);`の行で
`NS`という`Native.Signal`オブジェクトをmakeしている。
ついでに`Utils`というモジュールもmakeしている。


## Signal.Inputの作成

Signal.InputはInputとなるSignal型を作成する為の関数となる

`NS.input`の正確な使い方は、ドキュメンテーションされていないが、
[elm-lang/core/src/Native/Mouse.js](https://github.com/elm-lang/core/blob/3.0.0/src/Native/Mouse.js)あたりをみると類推する事が出来る。要は

- 第1引数にそのSignalの名前となる文字列
- 第2引数にデフォルトで返す値

を渡せばいいらしい。

値が必要なく、通知だけしたい場合は`()`を値として指定する。
`()`はJSの世界では`Utils.Tuple0`を渡せばいい。


```src/Native/WebSocket.js
  NS.input('WebSocket.onopen', Utils.Tuple0)
```

以上のように呼び出す事により、Elmの世界で使用可能な`Signal ()`型のオブジェクトを生成できる。

正確にはWebSocket.elm内で型アノテーションしてやる必要がある。

```src/WebSocket.elm
onopen : Socket -> Signal ()
onopen = Native.WebSocket.onopen
```

値が必要な場合は以下のようになる。

```src/Native/WebSocket.js
  NS.input('WebSocket.onmessage', "")
```

文字列型のデフォルトとして空文字列をデフォルトで宣言している。


これをelmの関数から使う際は

```example.elm
socket = WebSocket.socket

signals = Signal.mergeMany [ actions.signal
                           , Signal.map DecodeMessage <| WebSocket.onmessage socket
                           , Signal.map DecodeError <| WebSocket.onerror socket
                           ]
```

のように、Elm Architectureの`Signal Actions`に`Signal.map`を用いて変換した上で
`mergeMany`でマージする。


## コールバックの登録


Signal.Inputで作成したオブジェクトをmergeManyしただけでは、
いつまで待ってもSignalは飛んで来ない。

Signalを飛ばす為には`localRuntime.notify`を使う。引数は、

- 第1引数に`Signal.Input`で作ったオブジェクトの`id`を渡し、
- 第2引数に`Signal`と共に渡したい値を渡す

**そして、`localRuntime.notify`をラップした関数をコールバックとして渡す。**

```src/Native/WebSocket.js
socket.conn.onopen = function(e) { 
  localRuntime.notify(socket.onopen.id, Utils.Tuple0); 
};
```
以上で、コールバックを要求するAPIのJS側の実装は一通り出来る。

## 使用法

このように書かれたモジュールがどのような使用感になるかは
https://github.com/yasuyuky/elm-websocket/blob/master/example.elm
で確認して欲しい。

以下は余談。

## JSの世界とのインタラクションの為の`Json.Decode`/`Json.Encode`

elmはcoreに`Json.Decode`/`Json.Encode`モジュールを備えているが、
これは単純にJSON形式の文字列を解釈するためというよりは、
HTML/JS世界のオブジェクトとElmの世界のデータを相互に変換するための機構という
意味合いが強いような気がする。

つまり、JSのAPIでうけとるJSの値をJson.DecodeでElmの世界に持ってくる
もしくはその逆をする為にcoreにわざわざ入れているのだと思う。

実際に[evancz/elm-htmlのon関数](http://package.elm-lang.org/packages/evancz/elm-html/4.0.2/Html-Events#on)とを見てみたりすると、
`Json.Decoder`を引数としてとるような関数の作り方をしている。

今回、WebSocketの実装においては`onmessage`は`Signal String`型となっており、
JSONのデコードは呼び出し手に任せているが、
elm側で定義したdecoderを初期化時にJS側に引数として渡し、
API側でデコードさせる作りも考えられるかと思う。
（と、言うか型の事とか考えるとそっちのほうが良さそう。
　ただ現在のところElmのString型に関してはJSの文字列と
　互換という事もあり問題になってない）

# ElectronのIPCの例

みんな大好き[Electron](http://electron.atom.io)では、Desktopアプリの為の多様な[APIが用意されている](http://electron.atom.io/docs/)。

[ElmでElectronのアプリを作る方法については以前紹介した](http://qiita.com/yasuyuky/items/e28106e3dd7fed17d50f)が、
そこで紹介した方法ではElectronで動かすプロセス

- MainProcess(BrowserProcess)
- RendererProcess

のうち、RendererProcessの部分をElmで実装するものだった。
しかし、RendererProcessで使えるElectron APIは限られており、
Electronのポテンシャルを引き出すにはMainProcessとの通信が必須となる。

そこで使われるのが[ipcRenderer API](http://electron.atom.io/docs/v0.35.0/api/ipc-renderer/)だが、
ipcRenderer APIはコールバックを前提としており、
WebSocketのライブラリと同様に実装できる。
以下にElectron ipcRenderer APIの実装例を示す。

```Native/Ipc.js
Elm.Native.Ipc = {};

const ipcRenderer = require('electron').ipcRenderer;

Elm.Native.Ipc.make = function(localRuntime) {
  localRuntime.Native = localRuntime.Native || {};
  localRuntime.Native.Ipc = localRuntime.Native.Ipc || {};
  if (localRuntime.Native.Ipc.values) return localRuntime.Native.Ipc.values;

	var NS = Elm.Native.Signal.make(localRuntime);
	var Utils = Elm.Native.Utils.make(localRuntime);

  function ipc(c,encoder,decoder) {
    var ipc = {
      channel: c,
      on: NS.input('Ipc.on', Utils.Tuple0),
      encoder: encoder,
      decoder: decoder,
    };
    ipcRenderer.on(c, function(e, v){
      localRuntime.notify(ipc.on.id, ipc.decoder(v));
    })
    return ipc
  }

  function send(ipc,arg) {
    var ret = ipcRenderer.send(ipc.channel,ipc.encoder(arg));
    console.log(ret);
    return Utils.Tuple0
  }

  function sendSync(ipc,arg) {
    var ret = ipcRenderer.send(ipc.channel, ipc.encoder(arg));
    return ret
  }

  function on(ipc) {
    return ipc.on
  }

  return localRuntime.Native.Ipc.values = {  // Export
    ipc: F3(ipc),
    send: F2(send),
    sendSync: F2(sendSync),
    on: on
  };

};
```

```Ipc.elm
module Ipc where

import Native.Ipc

import Json.Encode
import Json.Decode
import Task

{-| Ipc struct -}
type Ipc = Ipc

{-| ipc constructor -}
ipc : String -> (a -> Json.Encode.Value) -> Json.Decode.Decoder b -> Ipc
ipc channel enc dec = Native.Ipc.ipc channel enc dec

{-| send ipc async -}
send : a -> Ipc -> Task.Task x ()
send v c = Native.Ipc.send v c |> Task.succeed

{-| send ipc sync -}
sendSync : a -> Ipc -> Task.Task x a
sendSync v c = Native.Ipc.sendSync v c |> Task.succeed

{-| wait on Signal a -}
on : Ipc -> Signal a
on = Native.Ipc.on
```


# まとめ

JavaScriptにおけるコールバックを要求するAPIを
Elmの世界で使う際にSignalを使って実装する方法について

- WebSocket API
- Electron ipcRenderer API

を例に説明した。

**みんなで色々なラッパーモジュール書いてフロントエンドはElmでやる世界を作ろう！**