Elmでやってみるシリーズ12:遅延ストリームで多段階選抜
Elmでやってみるシリーズ12:遅延ストリームで多段階選抜
横浜へなちょこプログラミング勉強会の過去問より、「多段階選抜 2014.8.2 問題というのをやってみます。以下が方針。
- 無限長データ構造を使えといわんばかりの問題である。Haskellならそのまんまである。しかし「いわゆる遅延評価」ではない正格評価のElmではそのままでは無限長データは使えない。なのでmaxsnew/Lazyというのを使用します(後述)。
- せっかくだからインタラクティブにしよう。
実行例
全画面はこちらからどうぞ。
以下はiframeでブログ記事内でインライン実行。なんかずれとりますが、全画面ではずれてません。
「記号列を入力」のところに例えば"ccc"と入力してみてください。
やってる内容は、多段階選抜 2014.8.2 問題を見てください。
ソースは以下もしくはこちら。
filter.elm
-- http://nabetani.sakura.ne.jp/hena/ord24eliseq/ module Filter where import Lazy.Stream as S import String (toList, fromList) import Graphics.Input.Field as F import Graphics.Input (Input,input,dropDown) import Char (toCode) import Maybe import Debug (log) -- 無限長の自然数ストリーム naturals : S.Stream Int naturals = S.iterate (\it -> it+1) 1 -- 初期リスト init : Maybe (S.Stream Int) init = Just naturals -- 入力文字に対応する各段階のフィルタとなる関数群 filter_n : Int -> S.Stream a -> S.Stream a filter_n n stream = S.map snd (S.filter (\(a,b)->(a `mod` n) /= 0) (S.zip naturals stream)) -- 2〜9 の倍数番目を撤去(先頭が1番目であることに注意) isSquare : Int -> Bool isSquare n=any (\x->n==x*x) [1..n `div` 2+1] filter_S : S.Stream Int -> S.Stream Int filter_S x = S.zip x (S.cons 0 (\_->x)) |> S.filter (\(a,b)->not (isSquare b)) |> S.map fst -- 平方数の次を撤去 filter_s : S.Stream Int -> S.Stream Int filter_s x = S.zip x (S.tail x) |> S.filter (\(a,b)->not (isSquare b)) |> S.map fst -- 平方数の直前を撤去 isCubed : Int -> Bool isCubed n=any (\x->n==x*x*x) [1..n `div` 2+1] filter_C : S.Stream Int -> S.Stream Int filter_C x = S.zip x (S.cons 0 (\_->x)) |> S.filter (\(a,b)->not (isCubed b)) |> S.map fst -- 立方数の直後を撤去 filter_c : S.Stream Int -> S.Stream Int filter_c x = S.zip x (S.tail x) |> S.filter (\(a,b)->not (isCubed b)) |> S.map fst -- 立方数の直前を撤去 filter_h : S.Stream a -> S.Stream a filter_h = S.drop 100 -- 先頭の100件を撤去 -- 入力文字に対応するフィルタ関数を返す。その関数について:入力文字が不正な文字(2-9,cCsSh以外)であったり、フィルタの入力がすでにNothingであった場合Nothingが返る。 char2func : Char -> Maybe (S.Stream Int) -> Maybe (S.Stream Int) char2func ch maybeStream = case maybeStream of Just stream -> if | '2'<=ch && ch<='9' -> Just (filter_n (toCode(ch)-toCode('0')) stream) | ch == 'c' -> Just (filter_c stream) | ch == 'C' -> Just (filter_C stream) | ch == 's' -> Just (filter_s stream) | ch == 'S' -> Just (filter_S stream) | ch == 'h' -> Just (filter_h stream) | otherwise -> Nothing Nothing -> Nothing -- 入力文字列に対応するフィルタ関数群を取得し、そのすべてをfoldlで関数合成したものに初期リストを適用して結果を得る solve : String -> Maybe (S.Stream Int) solve s = foldl (\ch acc -> char2func ch acc) init (toList s) -- フィルタ適用の各段階を表示する dispResultStep : Int -> (a, String) -> Element dispResultStep siz (ch, str) = flow down [flow right [asText ch, plainText "↓"] , solve str |> maybe (plainText "undefined") (asText . S.take siz) ] -- フィルタ適用の全段階を表示する dispResultSteps : Int -> String -> [Element] dispResultSteps siz xs = zip (toList xs) (allSteps xs) |> map (dispResultStep siz) -- フィルタ適用の途中段階用の入力文字列を生成 -- allSteps ["abc"] == ["a","ab","abc"] allSteps : String -> [String] allSteps x = let steps i x = map (\it -> fromList(i::(toList it))) x in foldr (\i acc -> [(fromList [i])] ++ (steps i acc)) [] (toList x) -- 入力文字列 filterString : Input F.Content filterString = input F.noContent -- 入力文字列フィールド filterField : F.Content -> Element filterField fldCont = F.field F.defaultStyle filterString.handle id "記号列を入力" fldCont -- 結果の幅 resultLength : Input Int resultLength = input 10 -- 結果の幅の選択入力フィールド resultLengthField : Element resultLengthField = dropDown resultLength.handle [ ("10", 10), ("20", 20) ] desc : Element desc = [markdown| [オフラインどう書く過去問題](http://yhpg.doorkeeper.jp/)[#24 多段階選抜](http://nabetani.sakura.ne.jp/hena/ord24eliseq/) <table border> <tr><th>記号<th>意味</tr> <tr><td>2〜9<td>2〜9 の倍数番目を撤去(先頭が1番目であることに注意)</tr> <tr><td>S<td>平方数の次を撤去</tr> <tr><td>s<td>平方数の直前を撤去</tr> <tr><td>C<td>立方数の直後を撤去</tr> <tr><td>c<td>立方数の直前を撤去</tr> <tr><td>h<td>先頭の100件を撤去</tr> </table> <br> |] -- 画面を構築 -- see:https://github.com/elm-lang/Elm/issues/523 main = let disp xs siz = flow down [ desc , (filterField xs `beside` plainText "長さ" `beside` resultLengthField) , (naturals |> S.take siz |> asText) , flow down (dispResultSteps siz xs.string) ] in disp <~ filterString.signal ~ resultLength.signal
気付いたことなど
- Haskellはデフォルト非正格評価だが、Elmは正格評価の言語である。このセマンティクス上の違いはいずれも純粋関数型であるが故に「おおむね見えない」のだが、実用的には以下のように表われてくる。
- (A) Haskellの場合、遅延評価に用いられるデータ構造と評価のタイミングにより、スペースリークの問題が生じ得ること。
- (B)Elmでは無限長データ構造がデフォルトでは扱えない
- (A)について、FRPの実装として、スペースリークが生じないことは、Elmが、Haskellのライブラリでもなく内部DSLでもなく、まさに今の形のように別言語であることの根本的な理由とされている。
- (B)について、無限データ構造は、Elmの非標準(コミュニティ)ライブラリの、遅延ストリームライブラリ「maxsnew/Lazy」を明示的に使用することで実現できる。しかし、
- Elmのロゴは「タングラム」を表わしていて、用途によってバリエーションを作るのが良いそうです。
- ElmのMaybeはモナドではない。なにしろ型クラスがないからね。それどころか(それゆえに?)、<~,liftなどは多相的に定義されていないので、アプリカティブ的にも使えない。困るかと思ったけどあまり困らない。Maybe.maybe便利。
- Lazy.Streamのconsのシグネチャは以下。なんで()->なの? なんで a->Stream a -> Stream aじゃないのだろうか? ご存知の方教えてください。
- cons : a -> (() -> Stream a) -> Stream a
関連エントリ
「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る
- 作者: Simon Marlow,山下伸夫,山本和彦,田中英行
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/08/21
- メディア: 大型本
- この商品を含むブログ (2件) を見る
Elmでやってみるシリーズ11:お絵描きツール
Elmでやってみるシリーズ11:お絵描きツール
今回は、マウスボタンをクリックすると、半径4の赤いドットが描画され、押下したままドラッグすると線が描けるというもの。以下が実行イメージ。
しかし今回は、いきなり完成させるのではなく、2ステップでやってみましょう。
ステップ1 マウスカーソルにドットが追随する
とりあえず、マウスカーソルにドットが追随するというもの。
上記を編集しての実行はこちらから。
ソースは以下のとおり。
import Mouse import Window -- マウスカーソル位置をElmのcollageの座標に変換する。collageの座標系は中心が原点でx軸は右が大きくy軸は上が大きい。マウスカーソル位置は左上が原点で、x軸は右に大きくy軸は下に大きい。 mousePosition : Int -> Int -> Int -> Int -> (Float, Float) mousePosition x y w h = (toFloat x-(toFloat w/2), -(toFloat y)+(toFloat h/2)) -- ウィンドウサイズと同じ大きさのcollageの中に次のようなFormを表示: circleで作ったShapeをfilledしたFormをさらにmoveでマウスカーソル位置に移動したもの。 main : Signal Element main = let disp x y w h = collage w h [move (mousePosition x y w h) <| filled red (circle 4) ] in disp <~ Mouse.x ~ Mouse.y ~ Window.width ~ Window.height
解説
これは本質的に、「マウスカーソルのx座標を表示しつづける」プログラム(→編集)
import Mouse main : Signal Element main = let disp x = asText x in disp <~ Mouse.x -- この2行は main = asText <~ Mouse.x と等価
と同型のコードです。Signal引数が増えて、あとElementの構成が複雑になっただけ。Elementの構成は、純粋なコード呼び出しと組立ての地道な問題です。
ポイントは「記憶すべき状態がないプログラム」の例だということです。マウスカーソル位置を保持する主体はElmコードの外にあります。Elmコードはシグナルの変化に対して受動的にその場でリアクションだけすればいい。
さらに、暗黙のループがあると想像するのも正しいです。Elmプログラム一般に、mainはシグナルが更新されるたび、実際に何回も呼ばれます*1。
ステップ2 軌跡を残す
さてステップ2として、マウスボタンが押下されたとき、画面にドットが残るようにします。
Elmのライブラリには「書いたらその情報がそのまま残るCanvas」という描画命令やデータ型は存在しません。ドット列全体の描画命令を再実行する必要があります。ドット列の情報は、マウスドラック操作によって追加されていくわけですから、プログラムは状態を保持しなくてはいけません。
そして、Elmでプログラムに過去からの状態を保持させる方法は、foldp、これ一択です*2。ドット列の情報は、座標位置x,yのタプルのリストとして保持するようにしましょう。
能書きは置いといて、とりあえず以下はshare-elmで動作中のコード。左ボタンを押してドラッグしてみてください。ソースや編集しての実行はこちらから。
コメントつきソースは以下です。
import Mouse import Window -- プログラムへの入力となる「イベント」を表現する代数的データ型Eventの定義。代数的データ型はとりあえずはenumのようなものと思っておけばよろし。 data Event = MouseUp | MouseDown | MouseMove Int Int -- プログラムが保持するすべての「状態」を表現するレコード型に型名Statusをつける。Haskellでは型シノニムと呼ぶが気にしなくていい。mouseDownはボタンが押されているときTrue。pointsは座標列。 type Status = { mouseDown:Bool, points:[(Int, Int)] } -- 初期状態(ボタンは押されてない、点は描画されてない) initialState : Status initialState = Status False [] -- 現状態stateからeventを受けて次状態を計算する。foldpの第一引数になることを想定。入力であるEventの中身を見て、対応する処理を行い、返り値のStatusを作ってリターンする。 nextState : Event -> Status -> Status nextState event state = case event of MouseUp -> { state | mouseDown <- False } MouseDown -> { state | mouseDown <- True } MouseMove x y -> if state.mouseDown then { state | points <- (x,y)::state.points } else state -- 入力列をシグナル化。mergeはホモジニアスなリスト、ここでは「EventのSignalのリスト」を要求するので、リストの要素の型をSignal Eventにそろえる。MouseMove、MouseDown, MouseUpは代数的データ型のデータ構成子だが、これは関数のようなものなので、直接リフト(<~)ができる。 inputSignal : Signal Event inputSignal = merges [ MouseMove <~ Mouse.x ~ Mouse.y , (\b -> if b then MouseDown else MouseUp) <~ Mouse.isDown ] -- foldpで過去を現在まで折り畳む。っていうか実質的にはイベントのシグナルの更新1回ごとにnextStateが呼ばれるように仕掛ける。この場合、nextStateはイベントハンドラだと思えばよろしい。状態はinitStateから始まり、nextStateの第二引数として、そしてそのnextStateのリターン値として持ち回る形で保持されていく。 currentState : Signal Status currentState = foldp nextState initialState inputSignal -- 先行のコードと同じ mousePosition : Int -> Int -> Int -> Int -> (Float, Float) mousePosition x y w h = (toFloat x-(toFloat w/2), -(toFloat y)+(toFloat h/2)) -- currentStateが返す「Signal Status」のStatus部分をとりだし、pointsを取り出してプロット。 main : Signal Element main = let disp state w h = collage w h (map (\(x,y) -> move (mousePosition x y w h) <| filled red (circle 4)) state.points ) in disp <~ currentState ~ Window.width ~ Window.height
気付いたことなど
- 結構長いが、「currentState = foldp nextState initialState inputSignal」は、ほぼ完全に定型コード。あとはここを起点として呼ばれている、nextState,initialState,inputSignal、そして使われている型Event,Statusをしかるべく定義すれば良い。
- MVCへの対応付けは以下のとおり。
- Statusが全てのモデルに相当する。nextStateは全モデルの更新(純粋関数型なので、実際には更新せずに新規にデータを作ってる)を賄う。
- currentStateが返す、シグナルでラップされたモデル(Status)をビューで表示するのは、main中のdispの役割り
- コントローラにはInput,Field,Buttonなどの入力部品(今回はない)と、シグナルの依存関係の構築(上ではinputSignal)が対応するであろう。
- Signal更新のたびに全ドット書いてるはずだが、点が多くなったときの速度は大丈夫だろうか*3。ビューポート指定して部分書き換えとかはできてないし、それがうまくできるかはわからない。
- UI構築のために、Graphics.Collage,Graphics.Elementまわりの型をひととおりは覚えておく必要がある。最終的にはmainの型であるElementを作らないといけないのだが、以下の流れ:
- 基本図形(丸とか四角とか多角形とか)
- Shapeを(rect/circle/..)で作って、filledでFormにして、(move/rotate/..)で変形させて、collageでElementへ
- 点列から
- (segment/path)でPathを作って、LineStyleを与えてtraceでFormにして、(同上)
- 文字列から
- (plainText/asText)でStringから直接Element作る
- Textを(toText)でStringから作り、(leftAligned/rightAligned/centered/..)でElement作る
- Textを(link/monospace/..)で変換してTextを作り、同上
- Textを(style)でStyleを与えて変換してTextを作り、同上
- Elementから
- Element同士をflow (right/down)..や`beside`などで繋げてElementにする。
- Markdownから
- [markdown|..|]でElement作る
- :
- 基本図形(丸とか四角とか多角形とか)
関連エントリ
「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る
*1:正確に言うと、mainが返すシグナルが保持するリフトされた純粋関数(ここではdisp)が何回も呼ばれる。どのタイミングで呼ばれるか? それは論理的には「すべてのシグナルのうちいずれか一つが更新したとき」なのだが、ある種の最適化のおかげで「常に呼ばれる」わけではなく、引数が変化しなければ引数としてキャッシュされた値が使用され、関数呼び出しはスキップされます。これはElmの関数が純粋だからできることです。純粋関数型万歳!!
*2:Singal.countもあるが、foldpで実現できる
*3:点の「追加」なので、差分更新が賢ければ速いはず。そしてelm-htmlのVirtual DOMはまさにそれをやってるはず。しかし、collageにそれができるか??
elmでやってみるシリーズ10: ボールの衝突回数で円周率を計算する
Elmでやってみるシリーズ10:ボールの衝突回数で円周率を計算する
id:wamanさんからひっそりと提案もらいましたので、やってみました。
今回は記事自身もElmで書きましたので、github-pages上の全画面(github上のソース)からどうぞ。
以下にも一応hatena blog記事中にiframeで表示しましたが、スクロールや文字の配置が読みにくい・もしくはJSの実行速度が遅いです。hatena blogではiframeにseamless属性つけられば良いのだが。iPhoneのSafariでもiframe中だとうまくいきませんが、全画面なら動くようです。
ソースはこちら。
PiByBall.elm
module PiByBall where import Window import Debug (log) import Graphics.Input (input, Input, button, dropDown) import Keyboard data Status = Pause | Running type State = { stat:Status, x1:Float, x2: Float, v1: Float, v2: Float, count: Int, ratio: Float } data Event = Start | Stop | TimeTick | ChangeN Int frameRate = 320 -- 画面更新頻度 initialState : State initialState = { stat=Pause, x1=-200, x2=-100, v1=1, v2=0, count=0, ratio=1 } inputSignal : Signal Event inputSignal = let f running = if | running -> Start | otherwise -> Stop in merges [(f <~ inpRunning.signal), (ChangeN <~ inpN.signal), (sampleOn (fps frameRate) (constant TimeTick))] colide v1 v2 r = (((r-1)*v1 + 2*v2)/(r+1), (2*r*v1 - (r-1)*v2)/(r+1), 1) nextState : Event -> State -> State nextState = \event ({stat, x1, x2, v1, v2, count, ratio} as state) -> case event of Start -> (log "Start" {initialState|stat<-Running, ratio<-ratio}) Stop -> (log "Stop" {state|stat<-Pause}) ChangeN n -> (log "ChangeN" {initialState|ratio<-100^toFloat n}) TimeTick -> if stat == Running then let (new_v1, new_v2, countIncl) = if | x1+v1>= x2+v2 -> colide v1 v2 ratio | x2+v2 >= 0 -> (v1, -v2, 1) | otherwise -> (v1, v2, 0) in (log "timetick" State Running (x1+new_v1) (x2+new_v2) new_v1 new_v2 (count+countIncl) ratio) else (log "Pause" {initialState|ratio<-ratio}) currentState : Signal State currentState = foldp nextState initialState inputSignal description1 = [markdown| ## ボールをぶつけるだけで円周率がわかる? ### シミュレーションの舞台 以下のように2つの質点M1,M2と壁を考えます。<br/> |] description2 = [markdown| 表示上の判り易さのために、質点の大きさに差を付けていま<br/> すが、表示されている大きさは質点の質量の比率には対応し<br/> ていません。 ### 質量について M1とM2の質量をそれぞれm1,m2としたとき、m1とm2の比率<br/> を以下とします。 m1:m2 = 100^N : 1 ここでNは0以上の整数値です。Nに応じて上記の比率は具体<br/> 的には以下のようになります。 <table border="1"> <tr><th>N</th><th>M1の質量(100^N) : M2の質量</th></tr> <tr><td>0</td><td>1 : 1</td></tr> <tr><td>1</td><td>100 : 1</td></tr> <tr><td>2</td><td>10000 : 1</td></tr> <tr><td>3</td><td>1000000 : 1</td></tr> <tr><td>:</td><td>:</td></tr> </table> ### シミュレーション 前提として、質点および壁は完全弾性衝突するとします。<br /> そして質点M1に右向きの適当な初速を与え、M2のM1および<br /> 壁に対する衝突回数をカウントします。</p> 実際にやってみましょう。まず以下でNは変更せずに(N=0の<br /> まま)「開始」ボタンを押してみて下さい。 |] description3 = [markdown| N=0のとき、衝突回数は最終的に3になったはずです。<br /> 「最終的」といっても計算の打ち切り処理はしてませんので、<br /> 永久に衝突しなくなるだろう時点を適当に判断してください。<br /> さらにNを1,2..と変えてみると、以下の結果になるでしょう。<br /> <table border="1"> <tr><th>N</th><th>衝突回数</th></tr> <tr><td>0</td><td>3</td></tr> <tr><td>1</td><td>31</td></tr> <tr><td>2</td><td>314</td></tr> <tr><td>3</td><td>3141</td></tr> <tr><td>4</td><td>31415</td></tr> </table> 注意深い読者は気づいたでしょうが、この回数が円周率に対応します。 <table border="1"> <tr><th>N</th><th>衝突回数c</th><th>c/10^N</th></tr> <tr><td>0</td><td>3</td><td>3.0</td></tr> <tr><td>1</td><td>31</td><td>3.1</td></tr> <tr><td>2</td><td>314</td><td>3.14</td></tr> <tr><td>3</td><td>3141</td><td>3.141</td></tr> <tr><td>4</td><td>31415</td><td>3.1415</td></tr> </table> Nを増やせば増やすほど、精度が上っていきます。 ### 留意点など - 初速は結果には関係ありません。 - 質点と壁の具体的な初期位置は結果には関係ありません(M1,M2,壁の順序で並んでい<br />て、M1の初速が右向きである必要はあります) - 衝突による速度の変化だけが結果を決めます。 - 正しい表示のためには、一定の離散時間でプロットするのではなく、時間精度を適<br />宜細かくとかしていく必要がありますが、このシミュレーションでは時間間隔一定<br />でプロットしています。動きが変なのは、そのせいです。 ### 参考その他 この記事はElmを使って書いています。この記事を紹介している記事は[こちら](http://uehaj.hatenablog.com/entry/2014/08/03/234120)。 以下を参考に(計算式は丸パクリ)させて頂きました。 - [「2つのボールをぶつけると円周率がわかる」らしいのでシミュレーションしてみた](http://wasan.hatenablog.com/entry/2014/04/10/073638) - [「2つのボールをぶつけると円周率がわかる」のをしつこく確かめてみた・・・解析的に](http://wasan.hatenablog.com/entry/2014/04/15/045611) |] bkcolor = rgb 200 200 256 inpN : Input Int inpN = input 0 -- Nを選択。 selectN : Element selectN = plainText "N=" `beside` dropDown inpN.handle [ ("0", 0) , ("1", 1) , ("2", 2) , ("3", 3) , ("4", 4) , ("5", 5)] inpRunning = input False startButton : Element startButton = button inpRunning.handle True "開始" stopButton : Element stopButton = button inpRunning.handle False "停止" -- シミュレーションを表示 simulation : Int -> Int -> State -> Element simulation w h state = layers [ collage w (h `div` 2) <| [move (-(toFloat w / 4), 0) <| filled bkcolor (rect ((toFloat w)/2) ((toFloat h)/2))] , flow down [ flow right [ collage w (h `div` 2) <| [ traced {defaultLine|width<-4} (segment (0, 200) (0, -200)) , move (min 0 state.x1, 0) (filled red <|circle 5) , move (min 0 state.x2, 0) (filled red <|circle 2) ] ] ] ] -- 画面を表示 main : Signal Element main=let disp w h state = spacer 10 10 `beside` flow down [ description1 , image 610 362 "fig1.png" , description2 , selectN , if state.stat == Running then stopButton else startButton , flow down [ "M1,M2の質量の比率(m1:m2)= 100^N:1 = "++show state.ratio++":1" |> plainText , "M1の位置="++show state.x1 |> plainText , "M2の位置="++show state.x2 |> plainText , "衝突回数:"++show state.count |> plainText ] , simulation w h state , description3 ] in disp <~ Window.width ~ constant 400 ~ currentState
補足
- Elmの次回リリースでは、Markdown interpolationというのができるようになるので、この手のはもっとみやすく書けるようになるでしょう。
- asパターン無いと思ってたらあった。キーワードasを使用します。上ではnextStateの引数「{stat, x1, x2, v1, v2, count, ratio} as state)」で使用。
- Signalを1つのEventストリームにマージして、Event->State->Stateという関数を作ってfoldpに与えて…という基本構造は、いろいろ考えても、おちつくところにおちつく。あんまりバリエーションが生じない気がする。そのための、ある種のフレームワークがいくつか提案されているようだが、今後調査してみたい。(→Playground, automaton)
- 古典的FRPでは、離散イベントを扱うEvent、連続的な変化を抽象化したBehaviorの2つで整理するようだが、Elmの採用するArrowizedFRPにおけるSignalは離散的であるという意味で古典的FRPのEventに対応している。SignalはEventのようにタイムスタンプを保持しているわけではないが、Time.timeStampでタイムスタンプを持ったSIgnalを生成することができる。Signal.sampleOnする先も離散的でよい(シグナルは最後の値1個を常に保持しているので値がとれないということはない)。Elmでは本質的に連続的に変化する値を扱うことはない。
関連エントリ
「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る
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
- 出力port: Elm→JS
JS側ではJSの機能は何でも使えるのですが、以下ではevalを呼んでみました。
以下は画面キャプチャ。
コードは以下のとおり。
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
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る
Groovy 2.4のgroovyshでは、defが効くようになった。
https://jira.codehaus.org/browse/GROOVY-6623
で対処されました。
% groovysh Groovy Shell (2.4.0-beta-1, JVM: 1.8.0_11) Type ':help' or ':h' for help. ------------------------------------------------------------------------------------------------------------------------------- groovy:000> def x = 3 ===> 3 groovy:000> x ===> 3 groovy:000> :set Preferences: interpreterMode=true groovy:000> x.class ===> class java.lang.Integer groovy:000> String x = 3 ===> 3 groovy:000> x.class ===> class java.lang.String groovy:000>
こうです。
(2014/07/31追記)
もちろん、defに限らず
groovy:000> String t = "abc" 000> String t = "abc" ===> abc groovy:000> t 000> t ===> abc
のように型を指定してもOKです。ローカル変数のスコープが、1行単位ではなく、セッションを通じて有効になる、という話。今までは一行ごとだった、つまり
groovy:000> String s="a"; s 000> String s="a"; s ===> a
はエラーにならなかった。
(2014/07/31追記終り)
前の動作に戻すには、
groovy:000> :set interpreterMode false groovy:000> def y = 3 ===> 3 groovy:000> y Unknown property: y
こうします。なんで今まで…、っていう話はあるんでしょうが、行ごとにnew GroovyShell()してたから、という実装の都合で、まあ、あれだ。
また、別件ですが、
https://jira.codehaus.org/browse/GROOVY-6754
によれば、
groovyshに、groovyコマンドと同様に-eオプションが指定でき、groovysh起動のタイミングで固有の初期設定コードが実行できるようになりました。
elmでやってみるシリーズ8: 赤いドットが回っているかのように見えるけど実は直進運動な錯視
Elmでやってみるシリーズ8: 赤いドットが回っているかのように見えるけど実は直進運動な錯視
少し前に、「このくるくる回る白いドット、実は真っ直ぐ往復してるだけなんだぜ」という記事がありましたが、これをElmで再現してみようというのが今回のネタ。
(追記 id:waman さんが数学解析をされています→回転しているようにみえる白いドットは単振動している - 倭算数理研究所 。このネタ自体もwamanさんのTwitterで知ったんですけどね。)
以下は画面キャプチャ(静止画)。
コードは以下のとおり。
fps_=96 -- 画面更新頻度 pi2 = 2*pi -- 2π deg30 = pi2 / 360 * 30 -- 30°をラジアン単位に変換 -- 1秒間にfps_回、1増加するシグナル値。 ticks : Signal number ticks = foldp (\it acc->acc+1) 0 (fps fps_) -- ticksから派生させて、1秒間に一回転に対応する角度値のシグナルを計算。また、元シグナルticksに対してdミリセカンド分、発生するのが遅れるシグナルにする sig : Float -> Signal Float sig d = let f x = (x / fps_) * pi2 in f <~ (delay (d * millisecond) ticks) -- 座標(x,y)を原点を中心に左にtだけ回転させた座標を得る rotate_ : Float -> (Float, Float) -> (Float, Float) rotate_ t (x,y) = (x*(cos t)-y*(sin t), x*(sin t)+y*(cos t)) -- 時間に追随するラジアン単位角度tに対するsin tの位置を、特定角度xで全体を傾けてプロットする dot : Float -> Float -> Form dot t x = move (rotate_ x (100 * (sin t), 0)) (filled red (circle 3)) -- 「tick値と、角度を変えたdotを6つはっつけたコラージュ(400x200)を作る純粋関数disp」に、dotの位置を表わすシグナル値sigを6つ作ってリフト適用。これらのシグナル値への引数としてそれぞれディレイ値を適切に設定することで、「遅れ」による位相差が生じ、「回転する」かのように見えることになる。 main : Signal Element main = let disp t p1 p2 p3 p4 p5 p6 = flow down [ asText t , collage 400 200 [ dot p1 <| deg30*0 , dot p2 <| deg30*1 , dot p3 <| deg30*2 , dot p4 <| deg30*3 , dot p5 <| deg30*4 , dot p6 <| deg30*5 ]] in disp <~ ticks ~ sig 0 ~ sig 100 ~ sig 200 ~ sig 300 ~ sig 400 ~ sig 500
気づいたこと・解説
- Signal.delay関数で元のticksのSignalを「時間的にずらしたシグナル」を作れる(sig dではついでにラジアン単位の角度にしている)
- おそらくJavaScriptのonTimerイベントの精度の関係で微妙な誤差が出るのですが、なんか有機生命みたいなゆらぎが出てておもろい。
- 角度でずらせば、正確にずらせるんですが、FRPの面白さを堪能するために、時間でずらしてます。
- hatena blog記事中のインラインフレーム中で実行させてると、また遅延が違うので動きが違う。味わい深い。
- 補助線を引いてみると以下のとおり。
- CanvasのAPIなんかまったく知らなくても、Elmライブラリのリファレンスだけ見れば使えるようになるのは偉大。
- iPhoneだと動かない。なんでや。
関連エントリ
「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る
elmでやってみるシリーズ7: elm-htmlでTwitter Bootstrapを適用
Elmでやってみるシリーズ7: elm-htmlでTwitter Bootstrapを適用。
つい先日、「Blazing Fast HTML」と銘打って、elm-htmlライブラリが公開されました。これはElmでDOMツリーを構築・更新するための低レベルライブラリであり、Virtual DOMという技術を使っているので非常に画面更新速度が速いそうです。SPA(Single Page Application)ではDOMの更新速度が重要になりますが、Elmは純粋関数型・イミュータブルデータなのでそのことを利用してさらに効率良く実装できるそうな。
従来、ElmはCSSとの連携はあまり重視されておらず、「ElmはCanvasを使ったアニメーションが得意」とされてきましたが、現代的な見た目のHTMLベースのアプリも自在に開発できるようになる、という道筋の第一歩なわけです。まだ未成熟ですがね。
個人的にTwitter BootstrapなどのCSSフレームワークとElmとの連携に興味があったので、今回試してみました。
やったことは以下のとおり。
- elm-getでelm-htmlをインストール
- elm-get install evancz/elm-html 0.1.2
- HtmlTest.elmを作る(後述)
- 以下のように--only-jsオプションを付けてelmコードをコンパイル。なお、--only-jsを使用する場合、呼び出すためにモジュール名が必要なので冒頭でmodule宣言が必要になる。
- elm --make --only-js HtmlTest.elm
- Twitter Bootstrapを使用するindex.htmlをこちらの「Embed in HTML」を見て適当につくる。index.htmlの内容はこちらを表示してビューソースしてみてください。
- index.htmlとHtmlTest.jsをgithub pagesにpush。
処理の内容としては、マウスのX座標、Y座標の過去10個分をテーブルにして表示するというもの。
コード(HtmlTest.elm)は以下のとおり。
module HtmlTest where -- --js-onlyをする場合モジュール宣言は必須 import Html (..) import Mouse -- マウス座標データを一行分の<tr>に変換 data2line : (Int,Int) -> Html data2line (x,y) = node "tr" [] [] [ node "td" [] [] [text <| show x] , node "td" [] [] [text <| show y] ] -- テーブルを作る tbl : [(Int,Int)] -> Html tbl dat = node "table" ["className" := "table table-striped table-bordered table-condensed"] [] [ node "thead" [] [] [ node "tr" [] [] [ node "th" [] [] [text "Mouse X"] , node "th" [] [] [text "Mouse Y"] ] ] , node "tbody" [] [] (map data2line dat) ] -- アンカータグによるリンクを作るユーティリティ関数 linkTo txt url = node "a" ["href":=url] [] [text txt] -- ボタンのように装飾をしたリンクを作るユーティリティ関数 buttonLinkTo txt url = node "a" ["href":=url,"className":="btn btn-primary btn-lg"] [] [text txt] -- 画面を作る body : [(Int,Int)] -> Html body dat = node "div" ["className":="navbar navbar-default navbar-fixed-top"] ["padding-top":= px 10] [ node "div" ["className":="container"] [] [ node "div" ["className":="jumbotron"] [] [ node "h1" [] [] [ text "Elm/Twitter Bootstrap" ] , node "p" [] [] [ "Elm-html" `linkTo` "https://github.com/evancz/elm-html" , text "で" , "Twitter bootstrap" `linkTo` "http://getbootstrap.com/" , text "連携しています。"] , node "p" [] [] [ "もっと学ぼう" `buttonLinkTo` "http://elm-lang.org/" , "ソースコード" `buttonLinkTo` "HtmlTest.elm" ] ] , tbl dat ] ] -- 画面を表示する display : [(Int, Int)] -> Element display list = body list |> toElement 200 200 -- マウス座標のシグナル値(x,y)をliftして与えて画面を表示する main : Signal Element main = display <~ foldp (\it acc -> take 10 (it :: acc)) [] Mouse.position
気づいたこと
- elm-htmlはGroovyのマークアップビルダーみたいなもの。
- elm-htmlの出力はHtml(DOM)であり、それを変更する手段は提供されていない。なのでコード的には毎回全体を何も考えずに宣言的に生成する。しかし、Elmが完全に純粋であることも利用して、Vitual DOMを通じて、実DOMに対して最低限の差分のみが効率良く適用されるという話。
- elm-htmlの記述はHTMLと一対一対応で、冗長度が高いが、elm-htmlの位置付けは、より高機能でより抽象度の高いライブラリ作成の基盤になるための低レベルライブラリ、というものなのでこれはこれで良い。
- elm-htmlはいわゆる非標準ライブラリであり、しかも--only-jsでHTMLと連携させるので、share-elmなどでは公開できない。なのでgithub-pagesを使用して公開してみた。
- --only-jsを前提とするとelm-serverでホットリロードできない。もしくはやりかたがわからない。
- elm-htmlのHtmlとGraphics.Input(.Fields)との連携・関係はまだ理解できていない。elm-htmlだけでhandleを使えるから、こっちで閉じてやるんだろうか。そうじゃないとしたらElementやfieldをHtmlに入れる仕組みが必要なはずだが。
- elm-htmlではcssも要素の属性も基本的には文字列でしかない。本来なら強い型付けをして欲しいものですが、将来開発されるであろうelm-html上に構築される何かに期待。
- 合成と共有部分の切り出し、共有化、抽象化というプログラミングの本質的な強力さが現状でも享受できる。html,js,cssを使っていたWebアプリ開発暗黒時代の夜明け
- Elmで外付けスタイルシートを定義することはできない。本来はElmがLESSやSassの代替になって欲しいものである。*1
- CSSクラスの指定は「"className":=」、floatは「cssFloat」とするらしい。このような違いはJavaScriptでDOMをいじる場合と同様らしい。
関連エントリ
「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る