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
- 出力port: Elm→JS
JS側ではJSの機能は何でも使えるのですが、以下ではevalを呼んでみました。
以下は画面キャプチャ。
コードは以下のとおり。
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
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る
elmでやってみるシリーズ5: 逆ポーランド電卓
いわゆる一つの逆ポーランド記法(Reverse Polish Notation, RPN)電卓です。入力フォームをSignalに結びつけて入力として使う例になります。私は本物のRPN電卓を使ったことがないので本物とはたぶん動作が違います。
import Graphics.Input (input, button) -- 電卓のボタン操作の種別 data Keys = Number Int | Add | Sub | Mul | Div | Clear | Enter -- GUIからの電卓に対する操作入力列を表現するシグナル。初期値はClear。 keys = input Clear -- 画面を作る。 calculator (n, xs) = let btn = button keys.handle btnN n = btn (Number n) (show n) in flow down [ flow right [ btnN 7, btnN 8, btnN 9, btn Add "+" ] , flow right [ btnN 4, btnN 5, btnN 6, btn Sub "-" ] , flow right [ btnN 1, btnN 2, btnN 3, btn Mul "*" ] , flow right [ btnN 0, btn Clear "Clear", btn Enter "Enter", btn Div "/" ] , if (n==0) then (asText xs) else (asText n) ] -- 有限状態マシン(状態=入力エリアへの入力中の値とスタックのタプル)としての電卓のモデル化。 calc : Keys -> (Int, [Int]) -> (Int, [Int]) calc it (num, xs) = case it of Number n -> (num*10+n, xs) Enter -> (0, num::xs) Clear -> (0,[]) _ -> if (length xs < 2) then (0, xs) else let top = head xs second = head (tail xs) rest = tail (tail xs) in case it of Add -> (0, (second+top) :: rest) Sub -> (0, (second-top) :: rest) Mul -> (0, (second*top) :: rest) Div -> (0, (second `div` top) :: rest) -- foldpで、入力操作列シグナルに対して、「1回前の電卓状態に、入力操作を適用し、次状態を生成する」という関数をリフト適用する。 main=lift calculator (foldp calc (0,[]) keys.signal)
実行画面(操作可能)
上記のソースを変更した上で実行したい場合はこちらからどうぞ。ブラウザ内で実行できます。
気づいたこと
- コロン(:)、ダブルコロン(::)はHaskellのそれとちょうど意味が逆になっている。既存のhaskell編集モードを流用したい場合、不便かも。
asパターンはない。asパターンは@ではなく、... as someのように記述。
NG: let (x@{a,b}) = {a=1, b=2} in x OK: let ({a,b} as x) = {a=1, b=2} in x
case式でリストをパターンマッチで分解できない。具体的には「case (x:xs) of ...」などができず、(x:xs)の部分は単一の変数名しか指定できない。(x:xs)を指定してもエラーにならないので、Elmのバグではなかろうか。(本件は記述が変なのと詳細を覚えてないので削除)- Inputのhandleは、複数のGUI部品からの入力を集約したSignalを作成するための間接参照点である。(という理解で正しいか?)
- Elmに例外ってないみたいだが、たとえば(head [])するとJavaScriptでの実行時例外になって、プログラムは再開できなくなる(もちろんページ全体をリロードすれば操作は再開できるが)。そうならないようにチェックすれば良いのだが、そういうものか?
- トップレベル定義の型アノテーションを型推論させて表示するには、Elmコマンドの-pもしくは--print-typesでできる。しかしアノテーションはあまり必要な気がしない(逆にアノテーションが本体関数の変更に追随しないことで良くエラーになる)が、ドキュメント情報としてみると便利であろう。
- foldlかfoldrで逆ポーランド電卓を作るのは、すごいH本の例にあったが、上のelmのはfoldpで同型。一般化すると、一連の入力シーケンスにともなって状態を変化させる処理は、適切な粒度のSignal+foldpの抽象化に対応するということか。ファイルからの入力文字ストリームはSignalにできそう。書き込みはどうか?ソケット通信やDBはどうか。
関連エントリ
「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
追記: Elm 0.14対応
import Graphics.Input (..) import Signal import Signal(Channel) import Graphics.Element (..) import Text(..) import List(..) type Keys = Number Int | Add | Sub | Mul | Div | Clear | Enter keys : Channel Keys keys = Signal.channel Clear calculator : (Int, List Int) -> Element calculator (n, xs) = let btn x = button (Signal.send keys x) btnN n = btn (Number n) (toString n) in flow down [ flow right [ btnN 7, btnN 8, btnN 9, btn Add "+" ] , flow right [ btnN 4, btnN 5, btnN 6, btn Sub "-" ] , flow right [ btnN 1, btnN 2, btnN 3, btn Mul "*" ] , flow right [ btnN 0, btn Clear "Clear", btn Enter "Enter", btn Div "/" ] , if (n==0) then (asText xs) else (asText n) ] calc : Keys -> (Int, List Int) -> (Int, List Int) calc it (num, xs) = case it of Number n -> (num*10+n, xs) Enter -> (0, num::xs) Clear -> (0,[]) _ -> if (length xs < 2) then (0, xs) else let top = head xs second = head (tail xs) rest = tail (tail xs) in case it of Add -> (0, (second+top) :: rest) Sub -> (0, (second-top) :: rest) Mul -> (0, (second*top) :: rest) Div -> (0, (second // top) :: rest) main=Signal.map calculator (Signal.foldp calc (0,[]) (Signal.subscribe keys))
elmでやってみるシリーズ1: ●を動かす
elmでやってみるシリーズ1: ●を動かす。
import Window pos : Signal Int pos =let f = \tmp -> (tmp `mod` 30) - 10 in f <~ foldp (\it acc-> acc + 1) 0 (fps 50) drawCircle : Float -> Float -> Form drawCircle x y = move (x,y) <| filled red (circle 20) drawMatrix : [Form] drawMatrix = map (\x -> drawLine <| x*10) [-10..10] drawLine : Float -> Form drawLine x = traced (solid blue) ( segment (x,-100) (x,100) ) main : Signal Element main=let f = \w h p-> collage w h ([drawCircle ((toFloat p)*10) 10]++drawMatrix) in f <~ Window.width ~ Window.height ~ pos
続く。
感想
関連エントリ
「プログラムでシダを描画する」をelmで描画する
やや乗り遅れているネタとして、シダを描くというのを、elm言語でやってみました。
(追記: 改良版も作りました)
elm言語は、基本はHaskellライク文法(サブセット方向)に、F#とOCaml風味の演算子・文法を振り掛けた、ヒンドリーミルナー型推論・純粋関数型・正格評価の言語で、repl上もしくは主にJSにコンパイルしてブラウザ内で実行します*1。特徴はFRP,ファンクショナルリアクティブプログラミングをサポートする言語だということです。
以下がシダを描画するelmコード。もっといい書き方あると思うので気付いたらご指摘お願いします。
import Mouse import Generator import Generator.Standard sida_width=500 sida_height=500 randomSeed = 12346789 gen = Generator.Standard.generator randomSeed main = collage (sida_width*2) (sida_height*2) <~ ((f0 13 0 0 gen) <~ (flip (/) sida_width <~ (toFloat <~ Mouse.x))) -- Original: w1x x y = 0.836 * x + 0.044 * y w1x x y n = n * x + (1-n) * y w1y x y = -0.044 * x + 0.836 * y + 0.169 w2x x y = -0.141 * x + 0.302 * y w2y x y = 0.302 * x + 0.141 * y + 0.127 w3x x y = 0.141 * x - 0.302 * y w3y x y = 0.302 * x + 0.141 * y + 0.169 w4x x y = 0 w4y x y = 0.175337 * y f0 k x y gen0 n = let (seq, _)=f k x y gen0 n in [(move (0,0)) (filled black <| rect (sida_width*2) (sida_height*2)) ]++seq f k x y gen0 n = if (0 < k) then let (rnd1, gen1) = Generator.int32 gen0 (seq1, gen2) = (f (k - 1) (w1x x y n) (w1y x y) gen1 n) (seq2, gen3) = if (rnd1 `mod` 3 == 0) then (f (k - 1) (w2x x y) (w2y x y) gen2 n) else ([], gen2) (seq3, gen4) = if (rnd1 `mod` 3 == 1) then (f (k - 1) (w3x x y) (w3y x y) gen3 n) else ([], gen3) (seq4, gen5) = if (rnd1 `mod` 3 == 2) then (f (k - 1) (w4x x y) (w4y x y) gen4 n) else ([], gen4) in (seq1 ++ seq2 ++ seq3 ++ seq4, gen5) else ((plot (x * sida_width * 0.98) (y * sida_height * 0.98)), gen0) plot x y = [move (x,y) (filled green <| rect 1 1)]
リアクティブプログラミングというのは、私の理解では、時間経過とかマウスクリックとか、なんらかの(外部/外部)イベントに刻々と反応することの記述を容易にできるってことです(雑すぎる説明)。それがファンクショナルなのは、関数型ってことで、elmはさらにピュア・ファンクショナルなので副作用は記述できません。Signalというのが「変化する値」を表わしていて、これはHaskellのIOアクションが副作用を一手に背負っているようなものです。SignalはしかしモナドではなくArrowで、これでFRPを実現しているのを Arrowized FRPと呼ぶとか。ただそこらへんの詳しいことはわからなくても書けます*2。
上記のコードは、マウスのx座標で反応するようにしました。FRPなのはmainだけであとは全部ピュアな関数です。x座標に対応して、シダの図のどのパラメータをどう動かすかは、適当にやったので動きは不自然です。あと再帰の深さを深くすると非常に遅くなります。Canvasに書き捨ててるんじゃなくて、描画命令(Form,形状)をリストで持ってるからです。
以下にはコンパイル結果のJSを登録しましたので、ブラウザ上で実際に動かして試すことができます。マウスを左右に動くとなんか動くわけです。
以下は、静止画像のスナップショットです(なので動きません)。
枝ぶりが貧弱なのは、速度上の問題ですorz。
以下はelm_dependencies.jsonの内容。標準関数以外には、random generatorというのを使っています。
{ "version": "0.1", "summary": "concise, helpful summary of your project", "description": "full description of this project, describe your use case", "license": "BSD3", "exposed-modules": [], "elm-version": "0.12.3", "dependencies": { "jcollard/generator": "0.3" }, "repository": "https://github.com/USER/PROJECT.git" }
elmについては別途紹介記事を書くつもり。