FRPを調べててelmに入門した話

これはElm Advent Calendar 2014の11日目です。

よく知らなかったFRP(Functional Reactive Programming,リアクティブプログラミング)について調べてて少しだけElmに入門しました。

※先週やった実装なのでElmのバージョンは0.13です。いろいろ変わったらしい0.14では動かない気がします。。

FRPとはストリームの写像

エクセルの例をよく見かけますが、FRPとは要するに入力および出力をストリーム(時間とともに変化していく状態)として、その間を写像(マッピング)していくプログラミング手法だとざっくり理解しました。

エクセル操作を例にストリームを写像するイメージの落書きをしてみました。

f:id:nihma:20141211010606j:plain

A3セルの定義がA1セルとA2セルの和になっていて、A1セル/A2セルを変更するとA3セルが定義に従って更新されていきます。

ここでは下記あたりを参考にしました。

【翻訳】あなたが求めていたリアクティブプログラミング入門

なぜリアクティブプログラミングは重要か。

The Reactive Manifesto 日本語訳

■ やってみる

良くわからないので適当な課題をでっちあげて実装してみました。FRPといえばHaskellライクなaltJSのElmらしい。というわけでElmを使ってみました。

● 課題

入力と出力が下記のようなGUIページを作ります。

入出力 要素 内容
入力a input/text 入力値
入力b input/text 入力値
入力c input/text 入力値
入力d input/text 入力値
入力e select 0〜3
出力a div ((入力ab))
出力b div [[入力dc]]
出力c div 入力eが0なら入力ababdcdc,入力eが1なら入力abdcabdc,入力eが2なら入力dcabdcab,入力eが3なら入力dcdcabab

● 設計

状態を依存グラフにしてみました。

f:id:nihma:20141211233945j:plain

● 実装

依存グラフを素直にコードに落としました。

-- elm test.elm
import Graphics.Input.Field as Field
import Graphics.Input as Input
import String
import Text
import Window

-- Signals and Inputs
in_a = Input.input Field.noContent
in_b = Input.input Field.noContent
in_c = Input.input Field.noContent
in_d = Input.input Field.noContent

in_e : Input.Input (Int)
in_e = Input.input 0

in_f_options : [(String, Int)]
in_f_options = [ ("0. ababcdcd", 0) , ("1. abcdabcd", 1)
               , ("2. cdabcdab", 2) , ("3. cdcdabab", 3)]

-- functions

main : Signal Element
main = printABCD
         <~ Window.dimensions
          ~ (selectABCD
               <~ (combineABCD
                     <~ (wrapAB <~ (addAB <~ in_a.signal
                                           ~ in_b.signal))
                      ~ (wrapCD <~ (addCD <~ in_c.signal
                                           ~ in_d.signal)))
                ~ in_e.signal)

printABCD : (Int, Int)
         -> (String, (String, String, String, String),
             (String, String, Field.Content, Field.Content),
             (String, String, Field.Content, Field.Content))
         -> Element
printABCD _ (select,
             (ababcdcd, abcdabcd, cdabcdab, cdcdabab),
             (w_ab, ab, in_a', in_b'),
             (w_cd, cd, in_c', in_d')) = 
    flow down 
     [ plainText "input a:", Field.field Field.defaultStyle in_a.handle identity "" in_a'
     , plainText "input b:", Field.field Field.defaultStyle in_b.handle identity "" in_b'
     , plainText "cache a: input a ++ input b", plainText ab
     , plainText "output a: (( ++ cache a ++ ))", plainText w_ab
     , plainText "input c:", Field.field Field.defaultStyle in_c.handle identity "" in_c'
     , plainText "input d:", Field.field Field.defaultStyle in_d.handle identity "" in_d'
     , plainText "cache b: input d ++ input c", plainText cd
     , plainText "output b: [[ ++ cache b ++ ]]:", plainText w_cd
     , plainText "input e:", Input.dropDown in_e.handle in_f_options
     , plainText "cache c(0):", plainText ababcdcd
     , plainText "cache c(1):", plainText abcdabcd
     , plainText "cache c(2):", plainText cdabcdab
     , plainText "cache c(3):", plainText cdcdabab
     , plainText "output c: cache c(input e)", plainText select
    ]

selectABCD : ((String, String, String, String),
              (String, String, Field.Content, Field.Content),
              (String, String, Field.Content, Field.Content))
           -> Int
           -> (String, (String, String, String, String),
               (String, String, Field.Content, Field.Content),
               (String, String, Field.Content, Field.Content))
selectABCD ((ababcdcd, abcdabcd, cdabcdab, cdcdabab), ab, cd) n =
            let s = case n of 0 -> ababcdcd
                              1 -> abcdabcd
                              2 -> cdabcdab
                              _ -> cdcdabab
            in (s, (ababcdcd, abcdabcd, cdabcdab, cdcdabab), ab, cd)

combineABCD : (String, String, Field.Content, Field.Content)
           -> (String, String, Field.Content, Field.Content) 
           -> ((String, String, String, String),
               (String, String, Field.Content, Field.Content),
               (String, String, Field.Content, Field.Content))
combineABCD (w_ab, ab, a, b) (w_cd, cd, c, d) =
       ((ab++ab++cd++cd, ab++cd++ab++cd, cd++ab++cd++ab, cd++cd++ab++ab),
        (w_ab, ab, a, b), (w_cd, cd, c, d))

addAB : Field.Content -> Field.Content
     -> (String, Field.Content, Field.Content)
addAB a b = (a.string ++ b.string, a, b)

wrapAB : (String, Field.Content, Field.Content)
      -> (String, String, Field.Content, Field.Content) 
wrapAB (ab, a, b) = ("((" ++ ab ++ "))", ab, a, b)

addCD : Field.Content -> Field.Content
     -> (String, Field.Content, Field.Content)
addCD c d = (d.string ++ c.string, c, d)

wrapCD : (String, Field.Content, Field.Content)
      -> (String, String, Field.Content, Field.Content) 
wrapCD (cd, c, d) = ("[[" ++ cd ++ "]]", cd, c, d)

動いたっぽいです。

f:id:nihma:20141212001803p:plain

ここら辺を参考にしました。

What is a Signal?

Using Signals

■ 感想

FRP/Elmともにいまいち理解が正しいのか分かりません。もう少し調査が必要な気がします。