uehaj's blog

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

elmでやってみるシリーズ3: xeyesっぽい目が動く

elmでやってみるシリーズ3: xeyesっぽい目が動く。

import Mouse

-- 目の輪郭を書く
eyeframe x y = move (x,y) <| outlined { defaultLine | width <- 5 } <| oval 40 50

-- 目玉を書く
eyeball x y = move (x,y) <| filled black <| circle 5

-- 目(=目玉+目の輪郭)を一個分書く。tは目玉の見ている方向。
eye r x y t = [eyeframe x y, eyeball ((r * (cos t))+x) ((r * (sin t))+y)]

-- 目を2つ書く
eyes t = collage 200 200 <| (eye 15 -20 0 t)++(eye 15 20 0 t)

-- 「目の座標位置を計算する(=「マウス座標の原点から見た角度」をatanで求める)」という純粋関数にマウス座標をリフト適用。シグナル化された目玉の角度が返る。
mouseDirec = let f x y = (atan2 (100-(toFloat y)) ((toFloat x)-100))
           in f <~ Mouse.x ~ Mouse.y

-- 目を2つ書く、という純粋関数に、シグナル化された目玉の角度(mouseDirec)をリフト適用する。
main = eyes <~ mouseDirec


この簡潔さよ! ブラボー。Elm ブラボー。(さぼってますけどね。)

ブラウザ内で編集したり実行したい場合はこちらからどうぞ。

気づいたこと

  • SignalはElmのFRPのキモである。Signalを制するものはElmを制する。
  • mainの直下にSignalを使うReactive Codeを固めて、他はなるべくピュアにしたい、と思うじゃない。でもそれはたぶん無理な相談である。いや、やれるところはそうすれば良いと思うけど、「Signalを返す関数」はElmプログラミングにとって中核的であり最重要。それをどう組むべきかという問題からは逃げられない。
  • 「なるべくピュアにしたい」という理由は、liftの適用は演算子<~,~があろうともやっぱり可読性に難があるから。今のElmにはセクションがないし、演算子関数をliftすると演算子っぽくなくなるし、flip駆使しても限界がある。もっと言えばSignalはモナドではないし*1、仮にモナドであってもElmにDo記法は無い。で、考えたのですが、ベストプラクティスとしてのコーディングパターンは、上でもしているように、lift一発でできないところは以下のように関数ごとに純粋部とリアクテイブ部に分けることではないかと今のところ思っております*2。なお今のElmはwhere句は使えません。
func = let f x y ... = 純粋コード
      in  f <~ Signal X ~ Singal Y ... -- 必要なSignalを使用するリアクティブコード

関連エントリ


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

*1:Signalがモナドでなくアプリカティブなのは理由があって、「SignalのSignal」を禁止するためだそうな。つまりSignalにjoinは定義できない・してはならない。bind/flatMapも然り。

*2:fは無名関数でも良いかもしれない。letのセマンティクスが無名関数とその適用に差があるのか、たとえばElmにlet多相があるかどうかは不明。

elmでやってみるシリーズ2: ●を往復させる

import Window
import Debug (log)

-- 旧速度vと、現在位置xから、新しい速度を求める。(はじっこだったら速度を反転する)
newv : Int -> Int -> Int
newv v x = if (x<= -10 || 10<=x) then -v else v

-- 状態を表わす(現在位置、速度)のタプルに対して、次の状態をシグナル(fps 20)のタイミングで累積的に計算しなおすということを繰り返す。初期状態は(0,1)。結果得られるのは、(現在位置,現在の速度)を表現するシグナルである。
pos = foldp (\it (x,v) -> 
            let nv = (newv v (log "x" x))
            in (x+nv, nv)
            ) (0,1) (fps 20)

-- 指定位置x,yを中心に●を書く(半径20の円を書いて赤で塗り潰す)。
drawCircle : Float -> Float -> Form
drawCircle x y = move (x,y) <| filled red (circle 20)

-- 一連の背景線を書く
drawMatrix : [Form]
drawMatrix = map (\x -> drawLine <| x*10) [-10..10]

-- 背景線を1本書く
drawLine : Float -> Form
drawLine x = traced (solid blue) ( segment (x,-100) (x,100) )

-- 「一連の背景線と、●を書いたコラージュを生成する純粋関数f」に、現在位置(posタプルの第一要素)をシグナル化したまま取り出してリフト適用
main : Signal Element
main=let f = \w h p-> collage w h ([drawCircle ((toFloat p)*10) 10]++drawMatrix)
     in f <~ Window.width ~ Window.height ~ (fst <~ pos)


解説的な何か

  • foldpのpは、pastのpで、その意味は実際に「過去から現在までのシグナルの履歴値を畳み込む」です。しかし、foldp使用に伴なうオーダーは実行開始時刻から現在までの時間経過には依存せず、O(1)です。実際には、シグナルの更新1回ごとに1回呼び出される関数を登録するというものです。foldpの第一引数の関数の第二引数(上で言う(x,v))は前回のfoldpの返り値を保持しています。この値が、いわゆる状態を保持・更新するためのelmにおいての唯一の手段です。
  • Debug.logは、JavaScriptでいうConsole.logを呼び出します。正格評価だからその時点の値が普通に出ます。

続く。

関連エントリ


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

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の<$>は<~、<*>は~に対応。