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でやってみるシリーズ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を使用するリアクティブコード
- 「{ defaultLine | width <- 5 } 」の記法は、指定したフィールドを更新した新しいレコードを返すというものである。Elmのレコードはすごく強力である。ちなみにレコードの拡張で型クラスを実現することも狙っているらしい。