uehaj's blog

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

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

このブログ記載のElmの記事で、動作するElmの実行プログラムをiframeで貼っていたのですが、いくつも動かなくなっていたので、動くようにしました。 ちなみに動かなくなっていた原因は2つありました。

  • Elmのコード共有サイトshare-elmの仕様がかわった。対処としてはリンクをはりなおしました。
  • elm-runtimeへのリンクが、src="http://elm-lang.org/elm-runtime.jsで参照していたのが、404エラーになるようになったため。そうなった正確な理由は不明ですが、バージョンに依存するはずなので、いずれにせよこの絶対リンクが上手くうごかなくなるのはあきらかでした。対処としてはgithub-page上の固有のものにリンクをはるようにした。

たいへん失礼しました。以下のとおりです。今はすべて動くはずです。

Elm関連記事へのリンクは、これからはこのエントリにも追記していきます。

書籍紹介

「Seven More Languages in Seven Weeks: Languages That Are Shaping the Future」にはElmの章があります。Elmの概要や作者インタビューなどもさりながら、他のどマイナー言語(IdrisやFactor, Julia, miniKanren等)の紹介も興味深かったです。

Seven More Languages in Seven Weeks: Languages That Are Shaping the Future
Bruce A. Tate Frederic Daoud Ian Dees Jack Moffitt
Pragmatic Bookshelf
売り上げランキング: 9,168

Elmでやってみるシリーズ12:遅延ストリームで多段階選抜

Elmでやってみるシリーズ12:遅延ストリームで多段階選抜

http://elm-lang.org/logo.png

横浜へなちょこプログラミング勉強会の過去問より、「多段階選抜 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>29<td>29 の倍数番目を撤去(先頭が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」を明示的に使用することで実現できる。しかし、
    • Lazyバージョン0.3がリポジトリに公開されていない。0.2にはたぶんバグがあって、今回の使途では止まってしまい結果が出ない。
    • Lazyバージョン0.3を自前でコピーして使用すれば上記問題は解決するが、1点、Elm 0.12の文法変更にともなう修正が必要。
    • 上記2つの理由により、現時点(2014/08/14)ではelm-getで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

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

プログラミングHaskell

プログラミングHaskell

Haskellによる並列・並行プログラミング

Haskellによる並列・並行プログラミング

elmでやってみるシリーズ8: 赤いドットが回っているかのように見えるけど実は直進運動な錯視

Elmでやってみるシリーズ8: 赤いドットが回っているかのように見えるけど実は直進運動な錯視

少し前に、「このくるくる回る白いドット、実は真っ直ぐ往復してるだけなんだぜ」という記事がありましたが、これをElmで再現してみようというのが今回のネタ。
(追記 id:waman さんが数学解析をされています→回転しているようにみえる白いドットは単振動している - 倭算数理研究所 。このネタ自体もwamanさんのTwitterで知ったんですけどね。)

以下は画面キャプチャ(静止画)。
f:id:uehaj:20140727232140p:plainf:id:uehaj:20140727232055p:plain

コードは以下のとおり。

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

実行画面

上記のソースを変更した上で実行したい場合はこちらからどうぞ。ブラウザ内で実行できます。フル画面表示はこちらから。fps_値を増やすとなめらかになりますが、ブラウザ負荷がたぶん上がります。

気づいたこと・解説

  • Signal.delay関数で元のticksのSignalを「時間的にずらしたシグナル」を作れる(sig dではついでにラジアン単位の角度にしている)
  • おそらくJavaScriptのonTimerイベントの精度の関係で微妙な誤差が出るのですが、なんか有機生命みたいなゆらぎが出てておもろい。
    • 角度でずらせば、正確にずらせるんですが、FRPの面白さを堪能するために、時間でずらしてます。
    • hatena blog記事中のインラインフレーム中で実行させてると、また遅延が違うので動きが違う。味わい深い。
    • 補助線を引いてみると以下のとおり。

関連エントリ


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

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

プログラミングHaskell

プログラミングHaskell

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との連携に興味があったので、今回試してみました。

やったことは以下のとおり。

  1. elm-getでelm-htmlをインストール
    • elm-get install evancz/elm-html 0.1.2
  2. HtmlTest.elmを作る(後述)
  3. 以下のように--only-jsオプションを付けてelmコードをコンパイル。なお、--only-jsを使用する場合、呼び出すためにモジュール名が必要なので冒頭でmodule宣言が必要になる。
    • elm --make --only-js HtmlTest.elm
  4. Twitter Bootstrapを使用するindex.htmlをこちらの「Embed in HTML」を見て適当につくる。index.htmlの内容はこちらを表示してビューソースしてみてください。
  5. 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

実行画面(操作可能)

全画面で実行する場合はこちら
プロジェクト全体はこちらのgh-pagesブランチからどうぞ。

気づいたこと

  • 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



すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

*1:と思ったが、そもそも、DOMを直接いじってスタイルが変更できるのだから、外付けスタイルシートなどはまったくいらなくなる…かとも思ったが、JSでトラバースしてスタイルつけるより、クラス名だけつけてブラウザに任せた方が速いと思うので、やっぱり何かしら必要かも。よくわからない。

elmでやってみるシリーズ6: 今日の天気予報を表示する(フォーム入力とWebAPI呼び出しとJSONで)

Elmでやってみるシリーズ6: 今日の天気予報を表示する。

今回は、以下の手順でお天気情報を表示します。

  1. Graphics.Input.dropDownとGraphics.Input.Field.fieldで、場所情報を入力させる
  2. 上記の2つはInputを共有させる
  3. 変更があったらHttp.sendGetでhttp://openweathermap.org/の各地天気を得るWeb APIを呼び出す
    • ここを選んだ理由は、認証が無いことと、CORSに対応するクロスドメインになってるからです。具体的には、レスポンスヘッダに「Access-Control-Allow-Origin: *」が付いているからです。
  4. 得られた結果をJSONとしてパースして、天気情報に相当する部分を取り出して表示する。

コードは以下のとおり。

import Http
import Graphics.Input
import Graphics.Input.Field as Field
import Json
import Dict

-- フィールドの入力値を抽象化したもの。
inp : Graphics.Input.Input Field.Content
inp = Graphics.Input.input Field.noContent

-- 文字列をフィールド入力値に変換(文字方向、テキスト選択領域などは使わないので適当に設定)
cont : String -> Field.Content
cont str =  Field.Content str (Field.Selection 0 0 Field.Forward)

-- 都市名を選択するセレクトフィールド。入力内容は、inpに伝播する。
citySelect : Element
citySelect = plainText "Select City:" `beside` Graphics.Input.dropDown inp.handle
        [ (""        , cont "")
        , ("Yokohama", cont "Yokohama,JP" )
        , ("Tokyo" ,  cont "Tokyo,JP")
        , ("Okinawa" , cont "Okinawa,JP") ]

-- 都市名を自由入力する入力フィールド。入力内容は、inpに伝播する。
cityInput : Field.Content -> Element
cityInput fldCont =  plainText "or Input City:" `beside`
   Field.field Field.defaultStyle inp.handle id "Please input city name" fldCont

-- 天気予報Web APIのURL
base : String
base = "http://api.openweathermap.org/data/2.5/weather?q="

-- 天気予報Web APIの呼び出し結果であるJSON文字列から、結果部分のみを抜き出す
getWeather : String -> String -> String
getWeather city body = case (Json.fromString body) of
  Just (Json.Object dict) -> case (Dict.get "weather" dict) of
    Just (Json.Array [Json.Object d]) -> case (Dict.get "description" d) of
       Just (Json.String description) -> "\n\nTenki of "++city++" is "++ description
    _ -> "nothing"
  _ -> "nothing"


-- 画面下部の結果表示部分
resultPart : String -> Http.Response String -> Element
resultPart city resp =
  if city=="" then plainText "Please select or input city"
              else case resp of
                Http.Success body -> plainText <| getWeather city body
                Http.Waiting -> image 16 16 "waiting.gif"
                Http.Failure _ _ -> asText resp

-- セレクトフィールドcitySelectもしくは入力フィールドcityInputのいずれかに入力された都市名のシグナル(inp.signal)に依存してHttp.sendGetを発行させ、その結果が画面下部に表示されるようにする(そういうシグナル間の依存関係を作る)。
main : Signal Element
main = let display city resp  = flow down [citySelect, cityInput city, resultPart city.string resp]
           toUrl city = base++city.string
       in display <~ inp.signal ~ (Http.sendGet (toUrl <~ inp.signal))

実行画面(操作可能)

上記のソースを変更した上で実行したい場合はこちらからどうぞ。ブラウザ内で実行できます。

気づいたこと

  • Http.sendGetの出力はいいとして、入力となるURLもSignalである。こうなってる理由はよくわからないが、
    • まずピュアな値をシグナルに変換するのはSignal.constantでも可能で簡単だから。Signalに対応させておけばピュア値もSignal値にも両対応になる、ということがあるかもしれない。Signalであれば、いちいちliftしなくてもInputの入力を繋げることができ、今回のような用途には向いている。
    • 次に、たぶんこれが正解だと思うのだが、ConcurrentFRP特有の事情がある。Elmの処理の実行は、変更伝搬を元にしている。Input Signal*1が変化したら、それに依存した依存Signal値を更新していく、という処理の流れをとることである。入力がSignalではなく、かつInput Signalでは無いSignalは、発火する(実行してそのSignalノードの値を更新する)タイミングがない。
    • signal引数が変化しないなら呼び出し自体を省略する、といった最適化の事情に関連があるかもしれないかと一瞬思ったが、haskellのIOから類推すると、返り値さえSignalであれば省略されないということが保証できそうなので却下。
  • Signalの取り回しはまだ私には難しい。結果的にはリアクティブコードがmainに集約できている。
  • 標準のJSON処理はあまりにも面倒。ダイナミックにレコード構文として扱えるような機能はないものか。
  • Graphics.Input.Fieldは、右から左に書く言語のための方向指定とか、セレクションの設定とかが必須で難しすぎる。単に文字列が得られるような簡易インターフェースも欲しい。
  • share elmは日本語が表示できない。try-elmなら問題ないんだけど保存とフルスクリーン表示ができないためshare-elmを使用。
    • 複数ファイルから構成するアプリや、imageなどをアップロードする必要がある場合、非標準(elm-getで取得する)のモジュールを使用する場合など、share-elmやtry-elmでは対応できない。いずれもこれはスニペット共有・勉強用の簡易なものという位置付けだからである。
    • だから、本格的なアプリを公開する際には、github-pagesなどを使う必要があるだろう。もちろんDB連携とかをするなら、そういうバックエンドサーバも動作させる必要がある。
  • 「型さえ合えばまず動く」という感覚がわかってきた。でも型を合せるのが大変。その意味では、前にトップレベル定義の型アノテーションはかならずしも必要ない、とか書いたけど自明ではないプログラムや、プログラムを機能拡張する場合などを考えると実質的には必須になるという気がしてきた。
  • case式でマッチしないものがあるとJSレベルでエラーとなる。「マッチしない可能性があるcase式」はエラーではないにせよ、警告してくれないものか。

関連エントリ


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


すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

*1:マウスやキー入力やタイマーイベントなど、Elmプログラムの外にあるものを直接の入力元とするSignal。

elmでやってみるシリーズ5: 逆ポーランド電卓

Elmでやってみるシリーズ5: 逆ポーランド電卓。

いわゆる一つの逆ポーランド記法(Reverse Polish Notation, RPN)電卓です。入力フォームをSignalに結びつけて入力として使う例になります。私は本物のRPN電卓を使ったことがないので本物とはたぶん動作が違います。

import Graphics.Input (input, button)

-- 電卓のボタン操作の種別
data Keys = Number Int | Add | Sub | Mul | Div | Clear | Enter

-- GUIからの電卓に対する操作入力列を表現するシグナル。初期値はClear。
keys = input Clear

-- 画面を作る。
calculator (n, xs) =
   let btn = button keys.handle
       btnN n = btn (Number n) (show n)
   in flow down [
        flow right [ btnN 7, btnN 8, btnN 9, btn Add "+" ]
      , flow right [ btnN 4, btnN 5, btnN 6, btn Sub "-" ]
      , flow right [ btnN 1, btnN 2, btnN 3, btn Mul "*" ]
      , flow right [ btnN 0, btn Clear "Clear",  btn Enter "Enter",  btn Div "/" ]
      , if (n==0) then (asText xs) else (asText n)
    ]

-- 有限状態マシン(状態=入力エリアへの入力中の値とスタックのタプル)としての電卓のモデル化。
calc : Keys -> (Int, [Int]) -> (Int, [Int])
calc it (num, xs) = case it of
  Number n -> (num*10+n, xs)
  Enter -> (0, num::xs)
  Clear -> (0,[])
  _ -> if (length xs < 2)
         then (0, xs)
         else let top = head xs
                  second = head (tail xs)
                  rest = tail (tail xs)
              in case it of
                 Add -> (0, (second+top) :: rest)
                 Sub -> (0, (second-top) :: rest)
                 Mul -> (0, (second*top) :: rest)
                 Div -> (0, (second `div` top) :: rest)

-- foldpで、入力操作列シグナルに対して、「1回前の電卓状態に、入力操作を適用し、次状態を生成する」という関数をリフト適用する。
main=lift calculator (foldp calc (0,[]) keys.signal)

実行画面(操作可能)

上記のソースを変更した上で実行したい場合はこちらからどうぞ。ブラウザ内で実行できます。

気づいたこと

  • コロン(:)、ダブルコロン(::)はHaskellのそれとちょうど意味が逆になっている。既存のhaskell編集モードを流用したい場合、不便かも。
  • asパターンはない。asパターンは@ではなく、... as someのように記述。
  NG: let (x@{a,b}) = {a=1, b=2} in x
  OK: let ({a,b} as x) = {a=1, b=2} in x
  • case式でリストをパターンマッチで分解できない。具体的には「case (x:xs) of ...」などができず、(x:xs)の部分は単一の変数名しか指定できない。(x:xs)を指定してもエラーにならないので、Elmのバグではなかろうか。(本件は記述が変なのと詳細を覚えてないので削除)
  • Inputのhandleは、複数GUI部品からの入力を集約したSignalを作成するための間接参照点である。(という理解で正しいか?)
  • Elmに例外ってないみたいだが、たとえば(head [])するとJavaScriptでの実行時例外になって、プログラムは再開できなくなる(もちろんページ全体をリロードすれば操作は再開できるが)。そうならないようにチェックすれば良いのだが、そういうものか?
  • トップレベル定義の型アノテーション型推論させて表示するには、Elmコマンドの-pもしくは--print-typesでできる。しかしアノテーションはあまり必要な気がしない(逆にアノテーションが本体関数の変更に追随しないことで良くエラーになる)が、ドキュメント情報としてみると便利であろう。
  • foldlかfoldrで逆ポーランド電卓を作るのは、すごいH本の例にあったが、上のelmのはfoldpで同型。一般化すると、一連の入力シーケンスにともなって状態を変化させる処理は、適切な粒度のSignal+foldpの抽象化に対応するということか。ファイルからの入力文字ストリームはSignalにできそう。書き込みはどうか?ソケット通信やDBはどうか。

関連エントリ


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


すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

追記: Elm 0.14対応

import Graphics.Input (..)
import Signal
import Signal(Channel)
import Graphics.Element (..)
import Text(..)
import List(..)

type Keys = Number Int | Add | Sub | Mul | Div | Clear | Enter

keys : Channel Keys
keys = Signal.channel  Clear

calculator : (Int, List Int) -> Element
calculator (n, xs) =
   let btn x = button (Signal.send keys x)
       btnN n = btn (Number n) (toString n)
   in flow down [
        flow right [ btnN 7, btnN 8, btnN 9, btn Add "+" ]
      , flow right [ btnN 4, btnN 5, btnN 6, btn Sub "-" ]
      , flow right [ btnN 1, btnN 2, btnN 3, btn Mul "*" ]
      , flow right [ btnN 0, btn Clear "Clear",  btn Enter "Enter",  btn Div "/" ]
      , if (n==0) then (asText xs) else (asText n)
    ]

calc : Keys -> (Int, List Int) -> (Int,  List Int)
calc it (num, xs) = case it of
  Number n -> (num*10+n, xs)
  Enter -> (0, num::xs)
  Clear -> (0,[])
  _ -> if (length xs < 2)
         then (0, xs)
         else let top = head xs
                  second = head (tail xs)
                  rest = tail (tail xs)
              in case it of
                 Add -> (0, (second+top) :: rest)
                 Sub -> (0, (second-top) :: rest)
                 Mul -> (0, (second*top) :: rest)
                 Div -> (0, (second // top) :: rest)

main=Signal.map calculator (Signal.foldp calc (0,[]) (Signal.subscribe keys))

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多相があるかどうかは不明。