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

elmでやってみるシリーズ5: 逆ポーランド電卓

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


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

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

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

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

追記: 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

続く。

感想

  • foldpは好き。
  • inputに対応するSignalってグローバル変数じゃね?まあ「マウスがクリックされる」とか「時間が経過する」という事実はグローバルなんだが、それによって引き起こされるイベントもそうなる…。プログラムからその値を能動的に制御できないから変数とは呼べない…のかな。でもユーザがフィールドに値を入力したり、ラジオボタンをクリックしたり、という操作によって設定項目をリアクティブに変更したその結果の、Signalである設定項目って、どう見てもグローバル変数だよね。まだよくわからない。

関連エントリ

「プログラムでシダを描画する」を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を登録しましたので、ブラウザ上で実際に動かして試すことができます。マウスを左右に動くとなんか動くわけです。

以下は、静止画像のスナップショットです(なので動きません)。

f:id:uehaj:20140703023021p:plain
f:id:uehaj:20140703023027p:plain

枝ぶりが貧弱なのは、速度上の問題です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については別途紹介記事を書くつもり。


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

*1:elmをAltJS、すなわちJSの他の選択肢(オルタネティブ)と言うべきなのかはわからない。言うならばJS+CSS+HTML+DOM全体のオルタネティブな気がする。

*2:ちなみにdo記法もproc記法もelmにはありません。lift(n)もしくはアプリカティブスタイルを使って書きます。Haskellの<$>は<~、<*>は~に対応。