言語ゲーム

とあるエンジニアが嘘ばかり書く日記

Twitter: @propella

SOE 15 章 A Module of Reactive Animations

The Haskell School of Expression の 15章 反応アニメーションを読みます。

** 環境設定

例えば Windows で本の例題を試す方法を書きます。
- http://www.haskell.org/soe/Packages/hugs_SOE-Jan2005.msi をインストール。
- http://www.haskell.org/soe/Packages/SOE.zip より資料を入手して展開。
- hugs を起動して、例えば以下のように SOE のディレクトリを設定する。
-- :set -P.;{Hugs}\lib\*;{Hugs}\libraries;{Hugs}\packages\*;{Hugs}\oldlib\;.\SOE\src
-- 一度設定すると設定はレジストリに記録されます。
-- これで、import で SOE のサンプルを導入出来ます。

** 15章の内容

反応アニメーション FAL の使い方と実装を学びます。FAL によって、マウス
に反応するようなプログラムを純粋関数言語で書く事が出来ます。特に興味深
いのが、イベント処理を無限リストとして扱う所です。これは Croquet の 
advanceTo: と同じ方法です。

** 静的アニメーション

まず動的アニメーションの前に、小手調べにマウス入力の無いアニメーション
を作ってみます。増えて行く時間を表示するつまらない例です(何時間もかかっ
たのは内緒)。

> module MyReactimate where
> import Fal -- FAL モジュール
> import Picture -- Region の定義
> import SOEGraphics -- 低レベル画像モジュール

Behavior というのは、移り変わる何かを表現する型です。Behavior String 
なら、「移り変わる文字列」を表します。time (Behavior Time) は移り変わ
る数字を表す関数で、これを文字列に変換して使います。任意のオブジェクト
を文字列に変換する show を使って、show time とやれば良さそうですが、こ
れだと String が返ってしまうので、Behavior String を返すには lift1 を
使います。

lift1 は、「数字を文字列に変える」という show の動作を「「移り変わる数
字」を「移り変わる文字列」に変える」」という動作に変換します。

> nowString :: Behavior String
> nowString = lift1 show time

次は馬鹿丁寧にやってますが、同じく lift1 を使い「文字を画像に変える」
という text 関数を、「「移り変わる文字列」を「移り変わる画像」に変える」
に変換します。text は座標を表す引数が要るので where を使っていますが、
カリー化を使って nowGraphic = lift1 (text (100, 100)) nowString とした
方が良いでしょう。

> nowGraphic :: Behavior Graphic
> nowGraphic = lift1 func nowString
>              where func string = text (100, 100) string

最後にウインドウの中に文字を表示します。(hugs で showNow を実行)

> showNow = reactimate "time"  nowGraphic

おまけ。前にご紹介した FtTime の lowering の技法を手動で行って lift1 
を減らしても良いです。この程度の最適化は GHC なら勝手にやるそうです。

> showNow1 = let nowGraphic = lift1 ((text (100, 100)) . show) time
>            in reactimate "time"  nowGraphic

** マウスに反応する。

前の例は「反応」アニメーションとはいい難いので、マウスクリックにより色
を変えてみます。本の例と同じですが、「移り変わる色」を Behavior Color 
で表します。次の行の変な定義は、左ボタンを押すと黄色に変わる事を表現し
ています。`untilB` のバックスラッシュは関数を中置記法で書く物ですが、
たまたま英語圏の作者はこの方が自然と感じたのでしょう。

> clickColor1 :: Behavior Color
> clickColor1 = red `untilB` (lbp ->> yellow)

今度は lift2 が出てきます。lift1 は、「移り変わるなんとか」の引数一つ
を変換する仕組みでしたが、lift2 は引数二つに使います。この場合、「移り
変わる色」と「移り変わる画像」を「移り変わる画像」に変換します。

> clickColorGraphic :: Behavior Graphic
> clickColorGraphic = lift2 withColor clickColor1 nowGraphic

クリックすると一度だけ黄色くなる動く数字を表示します。(hugs で showClickColor1 を実行)

> showClickColor1 = reactimate "time" clickColorGraphic

** 色々な移り変わる色

反応プログラミングの特徴は、「色」と、「移り変わる色」を別々に扱う事に
あります。移り変わる色、Behavior Color を色々な方法で作って行きます。
まず、クリックによって何度も変わる色です。まず clickColor2 は red (こ
れ自体 Behavior Color) で、次に yellow になり、次に再帰的に 
clickColor2 になります。という事でまた red に戻り、結局、赤 ->->-> ... と交互に色が変わってゆきます。

> clickColor2 = red `untilB` (lbp ->> yellow `untilB` (lbp ->> clickColor2))

と、思ったのですが、カッコは無くても OK だそうです。

> clickColor3 = red `untilB` lbp ->> yellow `untilB` lbp ->> clickColor2

表示してみます。

> showClickColor3 = reactimate "time" g
>     where g = lift2 withColor clickColor3 nowGraphic

次に、左クリックで青、キーボードで黄色というのをやります。

> showClickColor4 = reactimate "time" g
>     where c = red `untilB` ((lbp ->> blue) .|. (key ->> yellow))
>           g = lift2 withColor c nowGraphic

これでは一度しか色が変わらないので、untilB の変わりに switch を使いま
す。switch を使うと再帰を使わなくても繰り返しイベントを扱えます。

> showClickColor5 = reactimate "time" g
>     where c = red `switch` ((lbp ->> blue) .|. (key ->> red))
>           g = lift2 withColor c nowGraphic

本当の switch の実装はかなり違いますが、再帰でも書けるという事を確かめ
てみます。こっちの方が状態遷移の雰囲気がはっきりして好きです。

> showClickColor6 = reactimate "time" g
>     where myred = red `untilB` ((lbp ->> myblue) .|. (key ->> myred))
>           myblue = blue `untilB` ((lbp ->> myblue) .|. (key ->> myred))
>           g = lift2 withColor myred nowGraphic

では showClickColor3 の、交互に移り変わる色というのはどうやるのでしょ
うか? withElem_ を使うと、変更後の動作に無限リストを指定できます。

> colors :: [Behavior Color]
> colors = cycle [blue, red] -- [青、赤、青、赤、...]

> showClickColor7 = reactimate "time" g
>     where c = red `switch` (lbp `withElem_` colors)
>           g = lift2 withColor c nowGraphic

** イベント

知らないうちに新しい型が登場しています。`untilB` の後ろにあるのは 
Event (Behavior a) 型です。Event a は何かが起こるかも知れないという事
を表します。例えば lbp の型は Event () であり、そのままだと意味は「ボ
タンが押されても何も起こらない」です。key の型は Event Char で、「キー
を押すと文字が飛び出す。」です。

Event と ->> や withElem_ と組み合わせて、Event (Behavior a) を作ると、
意味は「イベントとその結果」になります。例えば lbp ->> red で「左ボタ
ンを押すと「移り変わる赤」になる」です。イベントとして、ずっと Event
(Behavior Color) を使ってきましたが、Event (Behavior String) の例を作っ
てみます。

まず、移り変わる文字列を表示する関数を用意します。

> showText :: Behavior String -> IO ()
> showText t = reactimate "text" (lift1 (text (100, 100)) t)

最初なので、単に固定した文字列を表示してみます。lift0 は、String から 
Behavior String を定数として作ります(本では、lift0 では無く constB が
使われますが、一貫性から言って lift0 の方が良い名前だと思います)。

> showText1 = showText (lift0 "hello, world")

クリックすると表示が変わる文字列を表示させてみます。

> showText2 = showText t
>     where t = lift0 "hello" `untilB` lbp ->> lift0 "world"

さらに、key :: Event Char の Char の部分を利用して、押されたキーによっ
て動作を変更させてみます。イベントの結果として、Behavior String の代わ
りに関数を書くので、->> ではなくて =>> を使います(こういう妙な演算子も
すべて FAL で定義されています)。

> eventText3 :: Behavior String
> eventText3 = lift0 "hello" `switch` key =>> handler
>     where handler 'a' = lift0 "Aho"
>           handler 'b' = lift0 "Baka"
>           handler _   = lift0 "Kusottare"

> showText3 = showText eventText3

** スナップショット

「何もしない」ことを表現する方法です。なぜこれが必要かと言うと、
eventText3 では handler で動作を決めていますが、キーが押されたら必ず何
かをしないといけないのです。というわけで、"Kusottare" ではなく「何もし
ない」事をするには、「基の値」を再セットしなくてはいけません。

> eventText4 :: Behavior String
> eventText4 = lift0 "hello" `switch` (key `snapshot` eventText4) =>> handler
>     where handler ('a', _)   = lift0 "Aho"
>           handler ('b', _)   = lift0 "Baka"
>           handler (_  , old) = lift0 old
> showText4 = showText eventText4

snapshot を使って、Event a を Event (a, 前の値) に変換しています。しか
し使い方は分かるのですが、どうやって動いているのかさっぱり分かりません。
eventText4 のどこに、「前の値」情報があるのでしょうか? このへんは多分、
FAL の実装を勉強すれば分かると思うので深入りしない事にします。

** 真偽イベント

Bahavior を Event に変換する方法です。例えば time の値をイベントに変換
してみます。while は、引数が False の時は何もしなくて、True になったと
たん永遠にイベントを発し続けます。

> eventText5 = lift0 "Sleeping..." `untilB`
>     while (time >* 5) ->> lift1 show time
> showText5 = showText eventText5

永遠ではなくて、一度だけイベントを発するには when を使います。と、思っ
たのですが失敗でした。この例では、「現在の時刻」ではなく、「変わり行く
時刻」がイベント結果になってしまうので、結局 while と変わりません。現
在の時刻を一度だけ表示する方法は後の課題とします。

> eventText6 = lift0 "Sleeping..." `untilB`
>     when (time >* 5) ->> lift1 show time
> showText6 = showText eventText6

** 積分

やっと eToys と関係ある話になってきました。値をちょっとずつ足してシミュ
レーションを作る方法です。

> eventText7 = lift1 show (integral (0.1))
> showText7 = showText eventText7

一見 time とどこが違うのかよく分かりません。。。なぜだ。

> showText8 = showText $ lift1 show $ time  * 0.1

とにかくこれでシミュレーションが作れるそうです。本に書いてあるやつより
単純化してみました。lift をことごとく省略できているのは、上手くポリフォー
ミズムが使われているからで、例えば x0 = -1.0 は x0 = lift0 -1.0 という
意味です。

> myball1 = test $ paint red (translate (x,y) (ell 0.2 0.2))
>        where x0 = -1.0             -- x 座標の初期値
>              y0 = 1.5              -- y 座標の初期値
>              g =  -3               -- 重力
>              v = integral g        -- 速度はだんだん増えて行く
>              x = x0 + integral 2.0 -- x 座標はだんだん増えて行く
>              y = y0 + integral v   -- y 座標は速度の分だけ増えて行く

hugs で myball1 を実行出来ます。折角なので time を使った版も。あ、そう
か。time だと足し合わせた値では無いので、引数が時間に応じて変化すると
意味が違うのかな。その割りに integral でも微妙な値が出るのはなぜだ。。。
よくわからん。

> myball2 = test $ paint red (translate (x,y) (ell 0.2 0.2))
>        where x0 = -1.0           -- x 座標の初期値
>              y0 = 1.5            -- y 座標の初期値
>              g =  -3             -- 重力
>              v = time * g        -- 速度はだんだん増えて行く
>              x = x0 + time * 2.0 -- x 座標はだんだん増えて行く
>              y = y0 + time * v   -- y 座標は速度の分だけ増えて行く

と、こんな所で、本当は一気に FAL の実装まで攻めようと思ったんだけど長
くなったので一旦休憩します。

ここまでの感想。僕が反応プログラミングの勉強をしている理由は、アラン・
ケイの要求する「スプレッドシート的なモデルを時間軸に沿って動かす」とい
う世界観を実現出来る唯一実現可能な方法だと思っているからです。今のとこ
ろ、この反応プログラミングを実用化するに当たって、1) Haskell じゃなく
て普通の言語でどう実装するか? 2) lift や snapshot が面倒くさい。の二つ
の問題を解決したら良いかなという気持ちでいます。ではでは。