はいはい。ども@nobkzです。
Elmが楽しいことを伝えられそうな記事を書きたいかなーとか思っています。
Elmでランダムウォークやった話をしまつー。
ランダムウォークとは?
一歩一歩、ランダムに、歩くことを言います。これを、Elmでシュミレートしました。
Elmでランダムォーク
walk関数を作る
まず簡単に、純粋な関数を作ります。walk関数は、数字0~3と、(Float,Float)を取り、(Float,Float)を返す関数として実装します。
walk choice (x,y) =
if choice == 0 then (x+1,y)
else if choice == 1 then (x-1,y)
else if choice == 2 then (x, y+1)
else (x,y-1)
表示関数を作る。
簡単に点を表示する関数です。型を考えると、(Float, Float) -> Elementを返す関数をつくります。
display pos = collage 600 600 [dotForm pos]
dotForm pos = filled black (ngon 4 5) |> move pos |> rotate (degrees 45)
では、試しに描画してみましょう。
main = display (0,0)
この様に表示されるはずです。
ちょっと分かりづらいですが、点が表示されているのが分かりますでしょうか?
Signal Valueと結合させる。
さてさて、まぁ、このまま動かない点を表示しても楽しくないですよね? このままだと、何も起きません。どうするのでしょうか? Elmは、そこで、Signal Valueを結合させて、インタラクションを産み出していきます。
Signalとは?
FRPでは、Signal Valueという、時が変わったり、ユーザがなにかアクションを起したときに、値が変化するSignal Valueというものがあります。
そして、Signalは、Signal a という型で表わされており、純粋な関数をlift関数などでラップしたり、Singal関数同士を結合させて、いろいろな、時の変化や、アクションに対応しようというわけです。
そして、値が変化したときに、プログラムが実行し、描画が変化していきます。
randomChoiceというSignalな値をつくる
さて、一定時間で、0~3の数字をランダムに変化するSignal Value、randomChoiceをつくりましょう。型はSignal Intです。
randomChoice = Random.range 0 3 <| fps 30
はい、これは30fpsで、ランダムに数字0~3の間で変化していく,Singal Valueです。ちょっと一旦、本当に値が時間単位で変わっているか、確認してみましょう。
-- <signal>と表示される!
main = asText randomChoice
はい、これは、signalと表示されるはずです! asText関数は、a -> Elementを取るの関数で、Signalの型の場合は、と表示するのですよね。
では、どうするのでしょうか? まず、Signalが時間変化の値を取り出したいのですが、静的で、純粋な関数型の世界では取り出せません。ではどうするのでしょうか?
答は簡単です。純粋な関数をSignalな関数に変化させれば良いのです。つまり、asText関数であれば、 a -> Elementを、Signal a -> Signal Elementの型の関数に変化させてあげるのです。それをやるのが、lift関数です!
lift関数は、(a -> b) -> Signal a -> Signal bという型の関数です。これを、asText関数に適用させて、randomChoiceの値の変化を見てみましょう。
main = lift asText randomChoice
30fpsで、0~3に値が変化していますね!
さて、walk関数randomChoiceを結合させ、displayする!
さて、さくっと、ランダムウォークするdotを完成させていきましょう。
おっと、またここで困りました。walkは、選択した数字と前の位置から、次の位置を返す関数でしたね。どうやって、前の位置を取り出せば良いでしょうか?
foldp
ここで、出てくるのが、foldpという関数です。
この関数は、(a -> b -> b) -> b -> Signal a -> Signal bという型の関数です。ちょっとややこしいですね。
foldと聞いてピンと来た方もいるでしょう、そうですこれはリストの畳み込みの関数を似たような動きをします!
さて、foldlを思い出してみましょう。試しに次のコードを書いてみました。最初に作ったwalk関数を使ってます。
foldl walk (0,0) [1,3,3,2]
さで、どのような動きをしてましたでしょうか?考えてみましょう。
1 まず、第二引数(0,0)と、第三引数のリストの先頭要素1を取ります、
↓
2 次にwalk に、(0,0)と1を適用して、(-1,0)が帰ってきます
↓
3 さて、(-1,0)を覚えておいて、次のリストの要素をとります。3です。
↓
4 覚えておいた、(-1,0)と3をwalkに適用して、(-1,-1)が帰ってきます。
↓
5 さらに、(-1,-1)を、そのリストの次の要素3を適用して、(-1,-2)になります。
↓
6 最後に、(-1,-2)と、リストの最後の要素と適用して(-1,-1)が変えってきます。
さて、Signal Valueというのは、時に寄って値が変化します。これを考えを変えてみましょう。
リストは、連続した値 ですね? 静的に順序があります。これに対して、
Signal Valueというのは、時に関して、連続した値と考えることができないでしょうか?
つまり、こう考えると、foldpの挙動が分かります。
foldp walk (0,0) randomChoice
さて、ここで、仮にrandomChoiceがこのように変化したとしましょう。
randomChoice => 1,3,2,1,0,1 .....
とすると、foldpというのは
foldl walk (0,0) [1,3,2,1, ....
に見えてきませんか?つまり、
1, 初期値(0,0)と、最初のSignalの値1をwalkに適用して、Signalの値(-1,0)になる、
↓
2、前の結果(-1,0)と、次のSignalの値3をwalkに適用して、Signalの値(-1,-1)になる、
↓
3、前の結果(-1,-1)と、次のSignalの値2をwalkに適用して、Signalの値(-1,0)になる。
このようになるわけです。えっと、ここで、foldpは、Signalの値を返すことに注意してください。
結合しましょう!
はい、ここまでくればあとは、簡単ですね。
main = foldp walk (0,0) randomChoice |> lift display
やりました! 完成です! なんとなく、dotが震える動きをしていたら、okでしょう!
軌跡を残す
はい、ランダムウォークは軌跡を残すと楽しいですよ。軌跡を表示してみましょう!
さて、では、軌跡の情報を残した、Signal Valueを作りましょう。簡単ですね。foldpで、貯めれば良いですね?
random_walker_trace = foldp walk (0,0) randomChoice |> foldp (::) []
そして、描画用も、displaysという関数を作っておきます。
displays = collage 600 600 << map dotForm
さて、 完成です!
main = displays <~ random_walker_trace
最終的なコードとウォークの模様
最終的にはこうなりました。
import Random
walk choice (x,y) =
if choice == 0 then (x+3,y)
else if choice == 1 then (x-3,y)
else if choice == 2 then (x, y+3)
else (x,y-3)
display pos = collage 600 600 [dotForm pos]
displays = collage 600 600 << map dotForm
dotForm pos = filled black (ngon 4 1) |> move pos |> rotate (degrees 45)
randomChoice = Random.range 0 3 <| fps 60
random_walker_trace = foldp walk (0,0) randomChoice |> foldp (::) []
main = displays <~ random_walker_trace
おまけ
複数で、沢山ランダムウォークするとこうなるよ!