LoginSignup
7
7

More than 5 years have passed since last update.

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

Posted at

この記事は Elm Advent Calendar 2015の14日目の記事として書かれました。

tl;dr

Elmからコールバックを要求するJavaScript APIモジュールの実装方法について検討し、
そのProof of ConceptとしてWebSocketのモジュールを作った

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

目標

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

対象読者

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

対象バージョン

Elm Platform 0.16.0

免責

ここで紹介している方法は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の中に用意されてる。

これを使うとなると、

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

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

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

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

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

以下、今回作成したWebSocket用モジュールを例に見ていくが、
ソースコードはそんなに長くないのでNativeモジュール書いた事のある人は
見るだけで理解できるかもしれない。

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

Native.Signalの準備

ElmのNative moduleにはSignalを作る為のJSの関数が用意されており、
NativeSignalモジュールを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あたりをみると類推する事が出来る。要は

  • 第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 ActionsSignal.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関数とを見てみたりすると、
Json.Decoderを引数としてとるような関数の作り方をしている。

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

ElectronのIPCの例

みんな大好きElectronでは、Desktopアプリの為の多様なAPIが用意されている

ElmでElectronのアプリを作る方法については以前紹介したが、
そこで紹介した方法ではElectronで動かすプロセス

  • MainProcess(BrowserProcess)
  • RendererProcess

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

そこで使われるのがipcRenderer APIだが、
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でやる世界を作ろう!

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7