uehaj's blog

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

Elmでやってみるシリーズ16: マウスストーカーを実装する

リアクティブプログラミングの技術を用いてマウスストーカーを実装する - はこべブログ ♨」という記事があり、興味深いのでElmのリアクティブプログラミングで似たようなことをやってみました。

全画面表示はこちらから。

コードは以下で、プロジェクト全体はこちらにあります。

import Text
import Window
import Time
import Mouse
import List
import Signal
import Graphics.Element(..)
import Graphics.Collage(..)
import Color(..)
import Signal(Signal,(<~),(~))
import AnimationFrame

-- マウスの座標をCollageの座標に変換するいつもの関数
mousePos : Int -> Int -> Int -> Int -> (Float, Float)
mousePos x y w h = (toFloat x-(toFloat w/2)
                   , -(toFloat y)+(toFloat h/2))

-- ★の表示
star : Int-> Int -> (Int, Int) -> Form
star w h (x, y) = Text.fromString "★"
                        |> Text.color orange
                        |> Text.centered
                        |> toForm
                        |> move (mousePos x y w h)

-- ビューの定義
view : List (Int, Int) -> (Int, Int) -> Element
view posList (w,h) = collage w h (List.map (star w h) posList)

-- ★の座標のリストのSignalを作る
stars : Signal(List (Int, Int))
stars = let 
          trace = Time.delay 100 -- 100ms遅延を与えたSignalを生成
          p1 = Signal.sampleOn AnimationFrame.frame Mouse.position -- 最初はマウス座標を追う
          p2 = trace p1 -- 以降、一個前の座標を追うようにする
          p3 = trace p2
          p4 = trace p3
          p5 = trace p4
          p6 = trace p5
          p7 = trace p6
          p8 = trace p7
          p9 = trace p8
          p10 = trace p9
          p11 = trace p10
          p12 = trace p11
          p13 = trace p12
          p14 = trace p13
        in (\a b c d e f g h i j k l m n -> [a, b, c, d, e, f, g, h, i, j, k, l, m, n])
         <~ p1 ~ p2 ~ p3 ~ p4 ~ p5 ~ p6 ~ p7 ~ p8 ~ p9 ~ p10 ~ p11 ~ p12 ~ p13 ~ p14

-- 表示関数viewをliftして★の座標をあてがう
main : Signal Element
main = view <~ stars ~ Window.dimensions

非常に簡潔です。

説明と注意点など

  • Mouse.positionおよびTime.delayが、シグナル(beacon.jsでいうEventStreamに相当)を生成しています。ちなみに次期Elm 0.15では、Signalは名称変更(Stream/Varyingに分割)が検討されているようです
  • 上記コードではマウス位置のサンプリング間隔を効率化するために AnimationFrameというコミュニティパッケージを使用しています。なので他のパッケージを使用する機能がないtry-elmshare-elmでは実行できません。sampleOnをfps 60とかですれば、もしくはsampleOnを使用せず直接Mouse.positionを使用すれば、AnimationFrameへの依存は除去できます。
  • 制限としては、ElmのCanvasベースのAPIであるGraphics.Collageを用いているため、★がifameの枠外まで追随することはありません。Nativeやportを用いてJSと連携すれば、真のマウスストーカーができるかもしれませんが試してはおりません。ちなみに次期版0.15ではJSとの連携能力が大きく変わるようです。
  • 上記コードでp1〜p14を列挙していますが、例えばmapを使用してリストを生成していないのは、Signalは動的に生成できないからです。Elmでは、すべてのSignalの値の依存関係に対応する「Signalグラフ」というものがコンパイル時に静的に決定されます。そのこともあり、不定件数の「Signalのリスト(List Signal a)」というものは作成できたとしても、(Signal (List a))に変換できないのです。sequence :: [m a] -> m [a] が欲しいところですが、そういう関数はなく、定義することも型制約*1で意図的に禁止されています。

余談

もうすぐ出るらしいElm 0.15は、非常に楽しみです。

参考リンク

Elmシリーズの他のエントリはこちら

*1:Signalはアプリカティブでモナドではない。

Elmでやってみるシリーズ15: Json.Decodeで9個以上のフィールドを持つobjectをデコードしてみる

本記事では、ElmのコアライブラリにおけるJSONデコードパッケージJson.Decodeを用いて、9個以上のフィールドをもつオブジェクトのデコード方法について説明する。

2015 08 31追記あり(andMapを使用する)

その前に

その前にJson.Decodeを簡単に説明する。 Json.Decodeは、JSONのデコード処理をするためのコアライブラリの一つである。Elm 0.14以前のものとは完全に置き換られている*1

Jsonデコード処理の概要

一般形式としては、

   decodeString : Decoder a -> String -> Result String a
   decodeString <デコーダ> <JSON文字列> = ...

である。decodeStringは、第二引数<JSON文字列>をJSON文字列としてパースし、<デコーダ>で指定した型のデコード結果を返却する。デコーダは1つの型パラメタaをとる多相型(Decoder a)の値であり、decodeStringにDecoder a型のデコーダを与えて呼び出したとき、パースが成功していたら、a型の結果がOk aとして返る。型の不一致やパース失敗のときはErr Stringが返る(Resultの型は、type Result error value = Ok value | Err error)。

Json.Decodeにおいて、JSON文字列からのパース処理は、基本的にdecodeStringから始まる。

Jsonデコード処理の例

以下はJsonデコード処理の例である。

import Json.Decode (..)
import Text (..)
import Graphics.Element (flow,down)
import List

main = flow down 
       [ asText <| decodeString string "\"abc\"" --(1)
        , asText <| decodeString int "3" --(2)
        , asText <| decodeString (list string) "[\"abc\",\"def\"]" --(3)
        , asText <| decodeString (list bool) "[true,false,true]" --(4)
        , asText <| decodeString (array bool) "[true,false,true]" --(5)
        , asText <| decodeString (tuple3 (,,) bool int string) "[true, 1, \"abc\"]" --(6)
        , asText <| decodeString ("a" := int) "{\"a\":1, \"b\":2}" --(7)
        , asText <| decodeString ("c" := int) "{\"a\":1, \"b\":2}" == Err ("expecting an object with field 'c' but got {\"a\":1,\"b\":2}") -- (8)
        , asText <| decodeString (object2 (,) ("a" := int) ("b" := string)) "{\"a\":1, \"b\":\"foo\"}" --(9)
        , asText <| decodeString (at ["a","b"] int) "{\"a\":{ \"b\":2 } }" --(10)
        , asText <| List.map (\it -> (decodeString it "{\"a\":1, \"b\":2}" )) [("a" := int), ("b" := int)] --(11)
       ]
  • (1),(2)におけるstringintなどはデコーダ型の値である。
  • (3),(4),(5)におけるlist, arrayなどはデコーダ型の値をとってデコーダを返す関数である。いずれもJson.Decodeで定義されている。list、arrayはいずれもJSONの配列からデコードするデコーダであるが、listは結果としてList型を、arrayはArray型の値を取り出す(デコードする)。このように、デコーダは「取り出したい型、結果として得たい型」を与えるものである。JSONの配列の要素の型は一致している必要がある。
  • (6)のtuple3は、3要素のJSONの配列と、それぞれの配列要素を引数としてとる3引数の関数を引数として与えると、その関数の結果をデコード型として得ることができる。「touple*」という名称だが、デコード結果として得ることができるのはタプル型に限らない。拡張レコード型だろうがリストだろうが別の関数を呼び出した何かの処理結果であろうが、なんでも御座れである。ただしたとえば返したい型がElmリストの場合は、デコード対象のJSONの配列のすべての要素の型が一致していないとデコードに失敗するであろう。
  • (7)の(a := int)は、オブジェクトのフィールドa(Int型)をデコードするデコーダである。該当するフィールドが存在しなければErrが結果として得られる(8)
  • (9)複数のフィールドを同時にデコードするにはobject<N>(Nは1〜8)を用いる。ここではint型フィールドaとString型フィールドbをデコードし、結果としてタプル(Int,String)の値を返している。
  • (10)atは、指定したフィールドaがオブジェクト型である場合、そのオブジェクトのフィールドbを取得して…のようにネストしたオブジェクトを順に辿っていくためのデコーダである。

9個以上のフィールドのデコードの問題

さて、オブジェクトをデコードする場合、上記(9)で使用したように、object<N>という関数を使用すると良い。問題は、Nが8までのしか定義されていないことである。少なっ!

9個以上のフィールドをデコードするにはどうしたら良いであろうか。一つの案は、ちまちまと:=を用いて、1個ずつフィールドを取り出せばよい。あるいは(11)のように何個かの:=に対してList.mapで一気にdecodeStringの結果を得ることもできる。しかし、decodeStringがパース処理の呼び出しそのものであり、効率が悪いという問題がある*2。さらに、結果が、Resultの配列なのも気になる。どれか1個がErrなら、全体がErrになってほしい。

この問題を解決する方法を以下に示す。

9個以上のフィールドをデコードする方法

まず、JSONのデコード結果として以下のような拡張レコードで結果を得たいものとする。タプルでも良いのだが、とりあえず。

type alias MyData = {
    f1 : Maybe String
  , f2 : Maybe Int
  , f3 : Maybe Float
  , f4 : Maybe String
  , f5 : Maybe String
  , f6 : Maybe String
  , f7 : Maybe String
  , f8 : Maybe String
  , f9 : Maybe String } -- 9個以上

そして、以下のようにヘルパ関数filedを準備する。

field : Decoder a -> String -> Dict.Dict String Value -> Maybe a
field typ fld dic = case (case Dict.get fld dic of -- (*)
                            Just val -> decodeValue (maybe typ) val
                            Nothing -> Err fld) of
                      Ok res -> res
                      Err _ -> Nothing

ここで(*)での「Dict.get fld dic」で得られるのは、Json.Decode.Value型の値で、そこから実際の値をとりだすためにdecodeValueを使用する。ここでのdicは後述の「dict value」デコーダを用いることで得られる、「JSONオブジェクトのデコード結果としてのDictの値」である。あとはちまちまとResultやMaybeでラッピングされた値をパターンマッチングでほどいて、最後にまたMaybeでラッピングしている。

このヘルパ関数を以下のように使用して、MyDataをデコードするデコーダを定義する。

myDataDecoder : Decoder MyData
myDataDecoder = customDecoder (dict value) (\dic ->
                                     Ok {
                                            f1 = field string "f1" dic
                                          , f2 = field int "f2" dic
                                          , f3 = field float "f3" dic
                                          , f4 = field string "f4" dic
                                          , f5 = field string "f5" dic
                                          , f6 = field string "f6" dic
                                          , f7 = field string "f7" dic
                                          , f8 = field string "f8" dic
                                          , f9 = field string "f9" dic
                                     })

deocdeStringに、デコーダdict value」を与えることが、肝心要の要点である。得られるのはキーがString、バリューがDict Json.Decode.Value型のDict(辞書)型の値のデコーダである。ちなみに、Json.Decode.Valueは、JSONのプリミティブや配列やObjectなどを直和したADTではなく、Valueという型引数も持たない、単一のデータコンストラクタだけを持つ型である。そんな型が何の役にたつかというと、これは実はNativeのJavaScriptの中だけで意味を持つものであり、Java Script側ではJSONのノードを保持している。Json.Decode.Valueは、本来その存在を気にする必要はないようにAPIが設計されているのだが、今回のように9個以上のフィールドを一気にデコードする、とかこういうときにはおおいに気にすることになるわけである。

上記デコーダmyDataDecoderの使い方は以下のとおり。

        decodeString myDataDecoder "{\"f1\":\"abc\",\"f2\":3,\"f3\":3.3,\"f4\":\"s\",\"f5\":null,\"f6\":\"s\",\"f7\":\"s\",\"f8\":\"s\",\"f9\":\"s\"}"

全体のコードはShare-elm上にも置いたが、以下のとおり。

import Json.Decode (..)
import Text (..)
import Graphics.Element (flow,down)
import List
import Dict

type alias MyData = {
    f1 : Maybe String
  , f2 : Maybe Int
  , f3 : Maybe Float
  , f4 : Maybe String
  , f5 : Maybe String
  , f6 : Maybe String
  , f7 : Maybe String
  , f8 : Maybe String
  , f9 : Maybe String } -- 9個以上

field : Decoder a -> String -> Dict.Dict String Value -> Maybe a
field typ fld dic = case (case Dict.get fld dic of
                            Just val -> decodeValue (maybe typ) val
                            Nothing -> Err fld) of
                      Ok res -> res
                      Err _ -> Nothing

myDataDecoder : Decoder MyData
myDataDecoder = customDecoder (dict value) (\dic ->
                                     Ok {
                                            f1 = field string "f1" dic
                                          , f2 = field int "f2" dic
                                          , f3 = field float "f3" dic
                                          , f4 = field string "f4" dic
                                          , f5 = field string "f5" dic
                                          , f6 = field string "f6" dic
                                          , f7 = field string "f7" dic
                                          , f8 = field string "f8" dic
                                          , f9 = field string "f9" dic
                                     })

main = flow down 
       [ asText <| decodeString string "\"abc\""
        , asText <| decodeString int "3"
        , asText <| decodeString (list string) "[\"abc\",\"def\"]"
        , asText <| decodeString (list bool) "[true,false,true]"
        , asText <| decodeString (array bool) "[true,false,true]"
        , asText <| decodeString (tuple3 (,,) bool int string) "[true, 1, \"abc\"]"
        , asText <| decodeString (tuple3 (\a c b->(a,b,c)) bool int string) "[true, 1, \"abc\"]"
        , asText <| decodeString ("a" := int) "{\"a\":1, \"b\":2}"
        , asText <| decodeString ("c" := int) "{\"a\":1, \"b\":2}" == Err ("expecting an object with field 'c' but got {\"a\":1,\"b\":2}") -- (9)
        , asText <| decodeString (object2 (,) ("a" := int) ("b" := string)) "{\"a\":1, \"b\":\"foo\"}" --(9)
        , asText <| List.map (\it -> (decodeString it "{\"a\":1, \"b\":2}" )) [("a" := int), ("b" := int)]
        , asText <| decodeString (at ["a","b"] int) "{\"a\":{ \"b\":2 } }"
        , asText <| decodeString myDataDecoder "{\"f1\":\"abc\",\"f2\":3,\"f3\":3.3,\"f4\":\"s\",\"f5\":null,\"f6\":\"s\",\"f7\":\"s\",\"f8\":\"s\",\"f9\":\"s\"}"
        , asText <| decodeString myDataDecoder "[]" == Err ("expecting an object but got []")
       ]

実行結果は以下のようになる。

Ok "abc"
Ok 3
Ok ["abc","def"]
Ok [True,False,True]
Ok (Array.fromList [True,False,True])
Ok (True,1,"abc")
Ok (True,"abc",1)
Ok 1
True
Ok (1,"foo")
[Ok 1,Ok 2]
Ok 2
Ok { f1 = Just "abc", f2 = Just 3, f3 = Just 3.3, f4 = Just "s", f5 = Nothing, f6 = Just "s", f7 = Just "s", f8 = Just "s", f9 = Just "s" }
True

他にも良いやりかたがあったらおしえてちょう!(NativeでやるとかPortでやるのは無しとして)

まとめ

decodeStringは内部でまずはJSONをパースして、いったんかならずJson.Decode.Valueのツリーが形成されているはず。なので、そこに対してデコーダをかませていく処理は、「ツリーをパースするVisitorパターン」でありDecoderがビジターである。そしてそのVisitor=Decoderは、パーサコンビネータライブラリのように合成していく。わかってしまえばなんということはない。 ない、のだが、動的言語とかでJson扱うのとは別次元の面倒さが発生していることは否定できないなー。

しかし、8は少ないだろ、8は…。

(追記) プルリクを打ってみた: object decoder for more than 8 fields · uehaj/core@3d49629 · GitHub

(追記2)

メーリングリストで以下のやりかたを教えてもらった。

myDataDecoder =
         ("f1":=int) `andThen` (\x1 ->
         ("f2":=string) `andThen` (\x2 ->
         ("f3":=float) `andThen` (\x3 ->
         ("f4":=string) `andThen` (\x4 ->
         ("f5":=maybe string) `andThen` (\x5 ->
         ("f6":=string) `andThen` (\x6 ->
         ("f7":=string) `andThen` (\x7 ->
         ("f8":=string) `andThen` (\x8 ->
         ("f9":=string) `andThen` (\x9 -> succeed {f1=x1,f2=x2,f3=x3,f4=x4,f5=x5,f6=x6,f7=x7,f8=x8,f9=x9})))))))))

Decoder.andThenはモナドのbind(>>=)です。なるほどねー。 でもこれはこれで見にくいし書きにくい。 do記法があれば

myDataDecoder = do
         x1 <- ("f1":=int)
         x2 <- ("f2":=string)
         x3 <- ("f3":=float)
         x4 <- ("f4":=string)
         x5 <- ("f5":=maybe string)
         x6 <- ("f6":=string)
         x7 <- ("f7":=string)
         x8 <- ("f8":=string)
         x9 <- ("f9":=string)
         succeed {f1=x1,f2=x2,f3=x3,f4=x4,f5=x5,f6=x6,f7=x7,f8=x8,f9=x9}

こう行ってたとこなんだけどね。 あるいは<~と~(Haskellの<$><*>)を定義して、

myDataDecoder = (\x1 x2 x3 x4 x5 x6 x7 x8 x9 -> {f1=x1,f2=x2,f3=x3,f4=x4,f5=x5,f6=x6,f7=x7,f8=x8,f9=x9})
                             <~ ("f1":=int)
                               ~ ("f2":=string)
                               ~ ("f3":=float)
                               ~ ("f4":=string)
                               ~ ("f5":=maybe string)
                               ~ ("f6":=string)
                               ~ ("f7":=string)
                               ~ ("f8":=string)
                               ~ ("f9":=string)

こう。

andMapを使う方法(追記2015/08/31)

以下で行けます。これが今のところベストなやりかたと思う。 参考: https://groups.google.com/d/msg/elm-discuss/J9ip8MqXtYM/u4aRGAlbSkcJ

import Json.Decode as Decode
import Json.Decode exposing (..)
import Text exposing (fromString)
import Graphics.Element exposing (flow,down,show)
import List

type alias MyData = {
    f1 : String
  , f2 : Int
  , f3 : Float
  , f4 : String
  , f5 : Maybe String
  , f6 : String
  , f7 : String
  , f8 : String
  , f9 : String }

andMap : Decoder (a -> b) -> Decoder a -> Decoder b
andMap = Json.Decode.object2 (<|)

myDataDecoder2 : Decoder MyData
myDataDecoder2 =
    MyData `Decode.map` ("f1":=Decode.string)
           `andMap` ("f2":=Decode.int)
           `andMap` ("f3":=Decode.float)
           `andMap` ("f4":=Decode.string)
           `andMap` ("f5":=Decode.maybe Decode.string)
           `andMap` ("f6":=Decode.string)
           `andMap` ("f7":=Decode.string)
           `andMap` ("f8":=Decode.string)
           `andMap` ("f9":=Decode.string)

main = flow down
       [ show <| decodeString myDataDecoder2 "{\"f1\":\"abc\",\"f2\":3,\"f3\":3.3,\"f4\":\"s\",\"f5\":null,\"f6\":\"s\",\"f7\":\"s\",\"f8\":\"s\",\"f9\":\"s\"}"
       ]

リンク

*1:以前のものの使用例はこちらでも紹介しているが、無用のものとなった

*2:本当に何度も呼びだすかは未確認。でもたぶん呼んでる。Elmにおいて、引数が同じ文字列で、純粋関数だからメモ化される、という期待はできない。さらにもちろんElmは遅延評価ではなくサンクなんかない。Signalにliftされた関数からsignal値としてのJSONが引数に与えられているなら、JSON文字列の更新がなければ多数回呼び出されないと考えてよいが、ここで考えている例の場合はあてはまらない。

Elmにおけるimportの使い方

Elmにおけるインポートの方法を説明する。本説明が対応するElmのバージョンはElm 0.14かそれ以降、ただしインポートの仕様は今後大きく変更される可能性がある(今後についての関連情報)。

他モジュールで定義され、エクスポートされた識別子(型コンストラクタ、値コンストラクタ、型エイリアス、定数・関数など)は、インポートすることで始めて使用可能となる。Elmのimportは、この点でJavaのimportと異なっている。Javaではimport宣言をしてもしなくてもFQCNを指定すればクラスパスにあるすべての識別子を参照可能であるが、Elmでは、インポートの明示的な宣言をしない限り、他モジュールの識別子を利用できない(ただし、いくつかのモジュール・識別子はデフォルトでインポートされている。後述)。

インポートの宣言をする方法は大きく分けて2つある。

  • Qualified Import
  • Open Import

(Open Importはunqualified importと呼ばれることもあるようだ)。いずれもソースファイル冒頭(モジュール宣言のすぐ後)に記述する。以降、それぞれについて説明する。

Qualified Import

一般形式:

  1. import モジュール名A
  2. import モジュール階層.モジュール階層.モジュール名B
  3. import モジュール名 as 短縮識別子C
  4. import モジュール階層.モジュール階層.モジュール名 as 短縮識別子D

説明:

Qualified Importは、モジュール指定もしくは短縮識別子をプリフィックスとして、他モジュールからexportされた識別子を利用する。たとえば、上記のようにimportを行ったとき、以下のように識別子をアクセスできる。

  1. モジュール名A.識別子
  2. モジュール階層.モジュール階層.モジュール名B.識別子
  3. 短縮識別子C.識別子
  4. 短縮識別子D.識別子

3,4の「短縮識別子」はこの記事を書く際に便宜上付けた名前であり正式名称は不明。 短縮識別子は短かい必要はないが1文字が多く使われる(Signal→Sなど)。4.のように階層のある名前例えば「Graphics.Element.Field」に「Field」という短縮識別子を付けるなどもできる。

短縮識別子は同じものを複数回指定できないので、Qualified Importでは識別子の衝突が発生しない。(モジュール名があらかじめ衝突していない前提であるが。)

中置演算子のインポートは、Qualified Importではできない。後述のOpen Importを使用する必要がある。

例1:

import Text

main=Text.plainText "abc"

例2:

import Text
import Graphics.Element

main=Graphics.Element.flow Graphics.Element.right [ Text.asText "ABC" ]

例3:

import Text as T
import Graphics.Element as Element

main=Element.flow Element.right [ T.asText "ABC" ]

例4

import Text(asText)
import Signal as S -- 短縮識別子S
import List as L -- 短縮識別子L
import Signal(..)

main =  S.map asText <| constant ( L.map (\x -> x+1)   [1,2,3]  )

Open Import

一般形式:

  1. import モジュール名A(..)
  2. import モジュール階層.モジュール階層.モジュール名B(..)
  3. import モジュール名C(識別子,識別子,識別子…)
  4. import モジュール階層.モジュール階層.モジュール名D(識別子,識別子,識別子…)

説明:

Open Importは、指定したモジュールで定義・エクスポートされた識別子をすべて(1,2のケース)あるいは列挙したもの(3,4のケース)を、importする側の名前空間に取り込んで、前置指定も無く利用できるようにする。識別子の衝突が発生する可能性があり、その可能性は、1,2>3.4である。

例5

import Text(..)

main=plainText "abc"

例6

import Text(..)
import Graphics.Element(..)

main=flow right [ asText "ABC" ]

例7

import Text(asText)
import List((::)) -- 中置演算子::のOpen Import
import Signal
import Signal(constant) -- Qualified ImportとOpen Importを組合わせる

main=(Signal.map) asText (constant (2::[3,4,5]))

型のOpen Importについて

判別共用体型(Tagged Union, ADT)は、「型コンストラクタだけをimportして値コンストラクタをインポートしない」「指定した値コンストラクタだけをインポートする」などが可能である。

Qualified ImportとOpen Importの使い分け

簡潔さの程度でいうと以下の順である(上の方ほど字数が少ない)。

  1. 識別子を指定せずにOpen Import
  2. 識別子を指定してOpen Inport
  3. Qualified importで短縮識別子使用
  4. Qualified importで短縮識別子使用しない

mapなどいかにも重なりそうな名前があるので、書き捨てコード以外は3か4、最悪でも2を選んでおくのが安全ではあるが、衝突は滅多に無い気もするな…はて。出現頻度が高いものは簡潔に書きたいので、importするモジュールごとに、そして書く内容によってそれぞれ判断したい気もする。elm-htmlとか既存のコードでどうやってるかを参考にするのも良いのではないかと。

デフォルトのimport

任意のElmプログラムで、以下が指定されているのと同じ動作をする。 (参考の下の方 )

  • import Basics (..)
  • import List ( List )
  • import Maybe ( Maybe( Just, Nothing ) )
  • import Result ( Result( Ok, Err ) )
  • import Signal ( Signal )

Listの::や、Signalの<~, ~が(Elm 0.14では少なくとも)デフォルトインポートされていないことに注意。前はElementとかSignal(..)などもデフォルトインポートされてたんだけどな…(´・ω・`)。

おまけ:

割と最近出た書籍「Seven More Languages in Seven Weeks: Languages That Are Shaping the Future」にはElmの章があるようです。ぽちっとな。

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でやってみる」シリーズのまとめエントリ - uehaj's blog

「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でやってみるシリーズ14:ライブラリを公開する

この記事は「Elm Advent Calendar 2014」の23日目として書きました。


f:id:uehaj:20141223193216j:plain:right:h320

今回は、作成したElmのライブラリをElmコミュニティライブラリに公開してみます。公開するブツは以前こっそりと作成してすでに登録していた「IntRange」というもので、たいしたものじゃございません*1。今回Elm 0.14に対応させた上で、elm-packageコマンドの登録手順を整理してみます。

プロジェクトを作る

何はともあれ、公開したいプロジェクトを作ります。
ディレクトリを作ってそこにcdしてelm-packageを実行。

$ mkdir IntRange
$ cd IntRange
$ elm-package install
Some new packages are needed. Here is the upgrade plan.

  Install:
    elm-lang/core 1.0.0

Do you approve of this plan? (y/n) y
Downloading elm-lang/core
Packages configured successfully!

これで以下のファイルが作られているはずです。

$ ls -la
total 8
drwxr-xr-x  4 uehaj  staff  136 12 23 09:34 ./
drwxr-xr-x  4 uehaj  staff  136 12 23 09:34 ../
-rw-r--r--  1 uehaj  staff  330 12 23 09:34 elm-package.json
drwxr-xr-x  4 uehaj  staff  136 12 23 09:34 elm-stuff/

Elmソースコードを作る

ライブラリ設計ガイド」を参考にしてがんばって作ります。このガイドで個人的に気になった内容を箇条書きにすると、

  • モジュールに定義した関数において、主要な処理対象のデータは関数の最後の引数にせよ。合成や部分適用で便利だからですね。
  • 中置演算子を避けよ。誰を非難するわけではないですが、おいそこらの言語! 聞いとけ。演算子乱用すんでね。検索できないし読めねー、プレフィックスで出自モジュールを明示することもできない。やめろ。もしやりたければ慣れた人向けのものとしてAPIの最後に置くか、別モジュール(ホゲ.Infix)に分けろ。
  • 抽象的すぎるAPIはよしとけ。汎用性が重要なのは認めるが、それは道具であって、設計上のゴールではなく、実際のユーザ便益が本当にあるのかどうか考え。その抽象性が有用な具体例を示せ。

ちなみにgitの使用が大前提です。バージョニングもgitを元にしています。githubが必須かどうかは不明。

elm-package.jsonを編集

JSONファイルを編集します。以下のような内容です。

version セマンティックバージョニングに基づいたバージョン番号が、ソース変更から自動的に設定されるので手動で修正する必要がない。セマンティックバージョニングについては後述。
summary 概要。修正してもサイトが更新されないケースがあるので、周到に決める必要がある。
repository このソースを格納しているgithubリポジトリURL。
license BSD3が推奨のようでデフォルトである。
souce-directory ソースコードの格納フォルダ。デフォルトは"."だがsrcとかを作っていればそこに。
exposed-modules 公開するモジュール名のリスト。手動設定が必要。
dependencies 依存ライブラリ。elm-package installで自動的に追加されるが手動で編集しても良い。

ドキュメントコメント

APIリファレンスの内容になるのでこちらのガイドに従って作ります。

ドキュメントコメントは、elm-docコマンドでJSONファイルが生成されます。このJSONは、package.elm-lang.org上でHTMLにレンダリングされるのですが、現状ではローカルで容易にプレビューする方法がないようです*2

$ elm-doc IntRange.elm
{
  "name": "IntRange",
  "comment": "The library provides fold*/map* to the range of numbers without consuming memory.\n\nIntRange.fold*/map* can be used as a replacement of List.fold*/map* on a list of consecutive Int values.\n\nUsing those method reduces memory when iterate on a list which have vast numbers of element and don't use the list after iterate.\n\nFor example,\n\n          import IntRange (to)\n          import IntRange\n          Import List\n\n          IntRange.foldl (+) 0 (0 `to` 100000000) -- Can be calculated without consuming less memory.\n          List.foldl (+) 0 [0..100000000] -- Require memory for the list which length is 100000000.\n\nBoth of List.foldl and IntRange.foldl don't consume call stack, but List.foldl allocate memory for the list whose length is 100000000. In contrast, IntRange.fold requires relatively less memory. It can be used like counter variable of loop.\n\n# Create IntRange\n@docs to\n\n# Iteration\n@docs foldl, foldr, map, map2\n\n# Convert\n@docs toList",
  "aliases": [],

  略

タグを切る

gitのタグを切っておきます。もしタグをつけておかないとelm-publishの際にエラーになり、タグを切れというメッセージが表示されます。

    git tag -a 1.0.0 -m "release version 1.0.0"
    git push origin 1.0.0

publish!

以下を実行することで、http://package.elm-lang.org/サイトにelm-package.jsonおよびelm-stuff/documentation.jsonの内容がPOSTされ、コミュニティライブラリとして登録されます。ちなみに、削除等の自動的な方法はおそらくない(個別に頼むしかない)と思うので、慎重にどうぞ。

$ elm-package publish
Verifying uehaj/IntRange 1.0.0 ...
This package has never been published before. Here's how things work:

  * Versions all have exactly three parts: MAJOR.MINOR.PATCH

  * All packages start with initial version 1.0.0

  * Versions are incremented based on how the API changes:

        PATCH - the API is the same, no risk of breaking code
        MINOR - values have been added, existing values are unchanged
        MAJOR - existing values have been changed or removed

  * I will bump versions for you, automatically enforcing these rules

The version number in elm-package.json is correct so you are all set!
Success!

バージョンアップ

さて、なんらかの理由でソースコードなどを編集した後で、バージョンアップしたものを登録する際には、「elm-package bump」を実行します。

$ elm-package bump
Based on your new API, this should be a PATCH change (1.0.0 => 1.0.1)
Bail out of this command and run 'elm-package diff' for a full explanation.

Should I perform the update (1.0.0 => 1.0.1) in elm-package.json? (y/n) y
Version changed to 1.0.1.

elm-packageは、ソースコードの公開APIを解析して、適切なセマンティックバージョニングに基づいたバージョン番号を付与をしてくれるのです。セマンティックバージョニングとは、詳しくはしりませんが、そのモジュールを利用する他のコード側から見て、バージョンアップしてよいかわるいかなどが判断できるような基準でのバージョン番号の付与ルールのようです。

セマンティクバージョンは「MAJOR.MINOR.PATCH」の3つの番号の組であり、1.0.0から始まり、APIの変更の種別によって

  • PATCH - APIとしては同一であり、互換性を破壊するリスクは無い
  • MINOR - 追加的な修正であり既存部には変更がない
  • MAJOR - 既存部分が修正されているか削除されている

という基準で増加させていきます。elm-packageでは、もちろん意味的な修正内容までを自動的に判断しているわけではないので、コンパイルが通るかどうか、という基準で考えれば良いと思われます。メジャーバージョンアップすると既存コードがコンパイルできる保証がなくなるということです*3

上記では、package.jsonをいじったぐらいなので、APIの追加も無いとみなされて、マイナーバージョンのみ増加しています。

ためしに一個、モジュールからの公開関数(toList)を削除してみると、

elm-package bump
Based on your new API, this should be a MAJOR change (1.0.1 => 2.0.0)
Bail out of this command and run 'elm-package diff' for a full explanation.

Should I perform the update (1.0.1 => 2.0.0) in elm-package.json? (y/n)

のようにメジャーバージョンアップとみなされます。
elm-package diffで以下のように理由を知ることができます。

$ elm-package diff
Comparing uehaj/IntRange 1.0.0 to local changes...
This is a MAJOR change.

ーーーー Changes to module IntRange - MAJOR ーーーー

    Removed:
        toList : IntRange -> List Int

公開するシンボルを一個増やしてみると、

$ elm-package bump
Based on your new API, this should be a MINOR change (1.0.1 => 1.1.0)
Bail out of this command and run 'elm-package diff' for a full explanation.

Should I perform the update (1.0.1 => 1.1.0) in elm-package.json? (y/n)

$ elm-package diff
Comparing uehaj/IntRange 1.0.0 to local changes...
This is a MINOR change.

ーーーー Changes to module IntRange - MINOR ーーーー

    Added:
        add : Int

たしかにマイナーバージョンアップになります。

まとめ、もしくは本題

Elmは、言語の存在目的/ミッションが非常にしっかりとした言語です。以下に私の考えるところのElmの存在意義を書いてみます。

一つは、「フロントエンド・プログラミングについてゼロの状態から考え直す」です。開発者である Evan Czaplicki氏へのインタビュー記事「【翻訳】フロントエンドのプログラミング言語をゼロから設計した理由 | POSTDを参照ください。

もう一つは、純粋関数型言語の産業界への普及です。Evan氏もしくはコミュニティの目的意識が非常にはっきりしていて、Elmに関する言語設計上の選択肢においては、「一般のプログラマが業務で使用できるようになる」ことを常に優先しています。

たとえば用語法について。ADTはADTと呼ばずUnion Typeと呼びます、elmのメーリングリストではHaksellでそうだから、という理由に流されず、常にゼロベースで議論がなされています。Elmにモナドの導入がなされるとしても、Elmにおいてモナドと呼ばれることはないでしょう。

Haskell文化圏は、「歴史的経緯」もしくは「背景となる数学理論」に基づいた名前が導入され永遠に使い続けられる傾向がある気がします。その種の用語に、誰もが最初は苦労したはずです。しかし、いったん慣れると、よしあしや合理性は別にしてそのわかりにくい名前を使い続けるのが通例です。ADT? Algebraic(代数的)って、どこがどういう側面について代数的なんだ? 直和型、直積型が、加算や乗算といった「代数的操作」に対応するからだとぉ! なんでとりたてて加算と減算だけを問題にする?減算・除算はどうなってるんだ??? とかみな思いますよね(2015/1/3削除)(「F-始代数」に由来するそうです(始代数 - Wikipedia)。型スキーマの都度具現化を意味するキーワードがなぜforallでなければならないかを納得できる理由に出あったことも私はありません。

もちろんHaskellはそれで良いです。目的が普及ではないからです。間接的に機能が他の「実用」言語に波及していけばそれが言語ミッションの成功です。あるいは、Haskellがそうであることは「Haskellが成功しすぎることの罠」に落ちないために必要だったのかもしれません。

Elmは別のレベルに目標を置きました。それが成功するかどうかはわかりませんが、自分個人も共感するので、それに寄与したいなあと思っています。プログラマと人類の幸福に寄与するために!
良いクリスマスを!

タングラムエディタ

間に合いませんでしたっ!

参考

関連エントリ


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

*1:Elm 0.13でList.indexedMapが追加されたので、明らかに存在意義が減じゃ。意味あるとしたらfoldl/foldrぐらいか。

*2:もしやるなら、elm-packageサイトをローカルに立てて、elm-package送信先を書き変えてコンパイルしなおして実行すればできるかもしれない。あるいはelm-docコマンドで生成したjsonを適当にレンダリングした方が早いかも。

*3:でもこの基準だとバージョン番号のインフレがおきそうですね。公開するシンボルは極小にするなど、API設計は慎重にやれ、ということか。

Elmでやってみるシリーズ13:あらためてシダを描く

間が少しあいちゃいましたが、実は続いていたこのシリーズ、「あらためてシダを描く」です。

f:id:uehaj:20140904230303p:plain

この図形は「バーンズリー(バーンズレイ)のシダ」と呼ばれる有名な図形で、以前各種の言語で実装するのが少し前に流行ったのが記憶に新しいところです。アルゴリズムなどについては詳しくはこちらをどうぞ。

実は、これについては、前にも「「プログラムでシダを描画する」をelmで描画する - uehaj's blog」で作ったのですが、当時はElmを良く知らずに試行錯誤で作ったものなので、現時点での知見をもとにして再度とりくんでみました。

このデモは計算量が大きく、さらにiframeでブログ内にインライン表示すると、なんらかの理由で非常に負荷が高くなる*1ので、今回はインラインデモは割愛します。全画面表示はこちら

ソースはこちら
ドラッグすると、パラメータが変更されて新たなシダ図形が描画さます。

ポイント

  • Main.elm(メイン), Leaf.elm(葉の表示)、Gauge.elm(ゲージの表示)の3モジュールに分割しました。Elmアプリケーションをモジュールに分割するにあたっては、Signalや状態更新の記述を独立性高く分割する方法について考えさせられました。
  • 結論から言うと、
    • モジュールに含まれるのが、純粋関数だけの場合には、思考すべきことはあんまりない。何を公開し何を隠蔽するかを吟味しておけば良い*2
    • モジュールが状態を保持する場合(そのコードにfoldpを含む場合)、モジュールごとにぞれぞれ以下のようにパートを分けて、単独実行可能なアプリケーションにする(参考:GameSkeleton.elm)。
      • Define constants
      • User input
      • State
      • Update
      • Display
      • puts it all together and shows it on screen.
    • その上で、メインプログラムからモジュールの状態更新(nextStateのような関数)、および現在状態を履歴から計算するfoldpの呼び出し等をどう扱うかについては以下から選択する
      • 定時間隔タイマ(fps,..)を使った状態更新が、機能上不要なモジュールであれば、モジュール側がliftしてSignal Elementを返しても良い。つまりイベントハンドラとステート管理をモジュール側で実装しても良い。
      • あるいは、定時間隔タイマを使うモジュールであっても、それを使うMain側が定時間隔タイマを使わないのであれば、ステート管理をモジュール側で実装してもよい。
      • しかし、モジュール側もMain側も定時間隔タイマを使う、もしくはMain側が定時間隔タイマを使うかどうか限定できない(一般ライブラリにする場合はそうなる)のであれば、両方でステート管理を実装すると、少なくとも現行Elmコンパイラが生成するJSコードにおいては性能劣化が激しい。なので、以下のようにする。
        • モジュール側では、
          • そのモジュールに関するStateレコードを定義し公開する
          • nextState, initStateに相当する処理を公開
          • 呼び出す側のMainでは、MainのStateレコードの1つのフィールドとしてモジュールのstateを含める。
        • 呼び出す側のMain側では、
          • nextStateの処理で、モジュール側のnextStateを呼び出し、MainのStateを更新する。
          • foldpを実行するのはMain側のみ。
      • その上で、モジュール側は、自分のfoldpを呼び出すmainを定義しても良い(Mainからは呼ばれない)。こうすることで、それぞれのモジュールを単独でも実行可能なElmアプリにできる。これはモジュールのデモや試験に有用。たとえば今回、赤のゲージはゲージ表示モジュール単独で実行できます(→Gauge.html)し、葉の表示モジュールでは、アニメーションや操作に反応しない葉っぱを表示させています(→Leaf.html)。
  • リアクティブっつーぐらいで、反応性重要
    • このための工夫として、ドラッグ動作中は描画・計算する点の数を減らすようにしました。
    • しかし、本来はこれは2モードにならなくても良いはずなのです。しかし残念ながら、現在のElmでは「計算中のマウスカーソル移動イベント」の検出の追随性が問題になります。(後述)
  • 前回同様以下の問題がありました。
    • 描画する点列の数nは実行を継続すればするほど増えていくのですが、Elmの標準ライブラリのGraphics.CollageおよびGraphics.Elementなどが提供するグラフィックスモデルは、ペイント系というよりドロー系で、描画されるデータが保持されるというものです。Canvasに書き残る、ということがないです(純粋だというわけですね)。
    • この結果、nに比例して描画時間が増えていきます。計算結果の点列([(Int,Int)])は、foldpで累積的に追加していけるので、O(1)なのですが、累積的に伸びた結果であるpoints:[Form]の描画時間はO(1)ではなく、O(n)なのです*3
    • この結果、何も考えないと、放置しておくとだんだん重くなってマウスクリックに反応しなくなります。回避策として以下を実施しています。
      • 1フレームの描画に要した時間を計測し、しきい値(例1秒)を越えるようなら、点列の追加ペースを落す(半減させる)。最終的に追加ペースは0になるので、そうなったら表示内容は収束し変化しなくなる。
      • 他の方針としては、まにあわなくなってきたら解像度とか計算精度落して、とかの戦略もありそうだったが、今回はできなかった。
      • 本来なら、Elm開発者Evanczの論文「Elm: Concurrent FRP for Functional GUI」によれば、Elmには「async」というプリミティブがあり、長い計算処理は適当な粒度で非同期イベントとしてイベントキューに再キューイングを行うことで、「ジャイアントUIスレッドロック」のようなことが起きなくなり、レスポンスを損なわずに長い計算ができるはずでした。しかし現状のElm実装にはasyncプリミティブはまだ実装されていません(そのことや理由も上記論文に書いてある*4し、解決されるべき課題として議論されている。)。(追記)asyncの代用として、Time.delay 0が使用可能だそうですが試してない。
    • 問題視してみましたが、例えば10000個とかの固定数で計算を打ち切れば良い話です。これをしなかった理由は、高速なCPUをもったマシンでは多数の点列を表示してゴージャスな表示、遅いマシンでは点数が少なくて、ちょっぴりみすぼらしいが表示はされる、というようなことを実現しようとしたかったからです。点数が多いと綺麗な表示になりますからね。
  • 諸事情によりElm 0.13のスナップショット版を使用。これはリリース版ではないので、自前でコンパイルしたい方は、googlegroupsのelm-discussionを見て適当にダウンロードしてください。

関連エントリ


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

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

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

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

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

プログラミングHaskell

プログラミングHaskell

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

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

*1:Chromeで発生、他のブラウザでは未確認

*2:ただし、Elmの現バージョンでは、エラーメッセージが不適切・もしくは全く出ないことがある(mainをexportしてない、拡張レコード型で暗黙に定義されるデータコンストラクタ関数が、個別export( (..)ではなく個々の関数名指定するケースで)未exportになる…)ので注意。

*3:virtual domのように、FormやElementから構成されるツリーの差分を検出し、差分だけをcanvas反映する機構があれば高速化できるかもしれませんね。でもいかにも難しそうですな。

*4:本質的には、JSにスレッドがないせいなんですが、将来的にはHTML5のWebWorkerでスレッドプーリングも併用するか、クロージャ本体を文字列にしてevalしてしかし引数をキャプチャ不要にしてなんとかとか。CPSに書き換えたりしてもできるんじゃないかな。

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による並列・並行プログラミング