uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

elmでやってみるシリーズ9: JavaScript連携(JSのevalを呼ぶ)

Elmでやってみるシリーズ9: JavaScript連携(JSのevalを呼ぶ)

Elmではportというものを使用することで、ElmからJSの機能を呼ぶことができます(→Ports: Communicate with JS)。
portは名前がJS側に公開されるSignalです。
portには入力portと出力portがあります。

  • 入力port: JS→Elm
    • Elm側では予約語portを指定して、型宣言のみをする。
      • port evalIn : Signal String
    • JS側から以下のいずれかの方法で値を送り込む。
      • 初期値を Elm.embed()などでマップで与える。
      • <Elm.embed()などが返すelmモジュール>ports.ポート名.send()で送る。
  • 出力port: Elm→JS
    • Elm側では予約語portを指定して、Signalを定義する。
      • port evalOut : Signal String
      • port evalOut = btnInp.signal
    • そのSignalをUI操作などで間接的に更新することでJS側で登録したハンドラが呼ばれる
      • 登録方法は、
        • <Elm.embed()などが返すelmモジュール>.ports.ポート名.subscribe(ハンドラ関数)
      • ハンドラ関数の引数にSignalがラッピングしている値が渡ってくる

JS側ではJSの機能は何でも使えるのですが、以下ではevalを呼んでみました。

以下は画面キャプチャ。
f:id:uehaj:20140730060010p:plain

コードは以下のとおり。

module PortTest where

import Graphics.Input (button, input, Input)
import Graphics.Input.Field as F

inp : Input F.Content
inp = input F.noContent

btnInp : Input String
btnInp = input "S"

fld fldCont = F.field F.defaultStyle inp.handle id "JSの式を入力して下さい" fldCont
btn fldCont = button btnInp.handle fldCont.string "Eval"

port evalIn : Signal String

port evalOut : Signal String
port evalOut = btnInp.signal

main : Signal Element
main = let disp cont bname = flow down [fld cont, btn cont, plainText bname]
       in disp <~ inp.signal ~ evalIn

上記を呼び出すHTMLは以下。

<!DOCTYPE HTML>
<html>

<head>
  <meta charset="UTF-8">
  <title>Call JS from Elm</title>
  <script type="text/javascript" src="http://elm-lang.org/elm-runtime.js"></script>
  <script type="text/javascript" src="build/PortTest.js"></script>
</head>

<body>
  <div id="portTest" style="width:50%; height:400px;" ></div>
</body>

<script type="text/javascript">
var div = document.getElementById('portTest');

// embed our Elm program in that <div>
elmModule = Elm.embed(Elm.PortTest, div, {"evalIn": "initial"});
elmModule.ports.evalOut.subscribe(jsEval);
function jsEval(exp) { elmModule.ports.evalIn.send(eval(exp).toString()); }

</script>

</html>

実行画面(操作可能)

全画面はこちらから。

気づいたこと・解説

  • JSからElmに渡される値の型は厳しくチェックされる。自動的にtoString()を呼んでくれたりはしない。
  • portは「値」ベースの情報交換である。直接相互の関数を呼んだりはできない。
    • Fregeと比べると興味深い。JSのpureな関数があってもそれをElmから直接を呼ぶことはできない(今のところ)
  • 他のaltJSに比べれば、一手間かかるわけだが、evalは万能インターフェース(文字列限定)
  • Elmにおいて煩雑に思われるJsonからの値のとりだしはJS側でやるという手もあるかもね。
  • シグナル間の依存関係は以下のとおり。
                                                                           evalOut:Signal String
                                                                                 ^
"Eval"Button                              btnInp:Input String                    /
btn:Element ==================================> handle:Handle String            /btnInp.signal
                                                signal:Signal String ----------/
                                                   ^
                                                   :inp.signal.string
                                                   :(fldCont = inp.signal;
JS Exp Field    inp:Input F.Content                : btnInp.signal = Signal (fldCont.string)
fld:Element ========> handle:Handle F.Content     /
                      signal:Signal F.Content ../


                             Result Text
evalIn:Singal String ------> plainText bname:Element

(凡例)
signalA -------> signalB
signalBはsignalAを参照している
signalAの値が変更されるとsignalBの値が再計算される
(情報の流れの向きと参照関係は逆)


ElementE =======> HandleH
ElementEはHandleHを参照している。
ElementEのフィールド入力値が変更されるとhandleHを保持するInputに所属するSignalで更新イベントが発生する
(情報の流れの向きと参照関係は同じ)


valueA ........> valueB
valueAがvalueBとして使われている。純粋な値のコピー。
  • btnを押したときに、evalOutへの出力はされるのに、fldを変更したときに、evalOutへ出力されないのはなぜか?それは、全体構造を良く見るとわかるように、fldが変更されたとき、inp.signalが変化し、その値をもとにbtnが再構築されるから。btnInp.signalはinp.signalに依存していない。inp.signal.stringの(純粋な)値を使って構築されている。

関連エントリ


「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog


すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

プログラミングHaskell

プログラミングHaskell