- Haskell Advent Calendar 2014 (18日目) 寄稿記事。
はじめに
独学でHaskellを学び始めて2年半が経ちました。圏論やモナド、その他重要な概念を何も理解できていませんが日々楽しくHaskellを使っています。そんな(初心者&底辺)のHaskellerが半年ほど前に無謀にも FRP(Functional Reactive Programming)*1に挑戦してみました。FRPのパッケージは幾つか存在しますが、一番元気良さそう(小並感)にみえたSodium*2 を選択しました。
悪戦苦闘の末、僅かばかりのノウハウを得られましたので、それに基いてSodiumの簡単な紹介記事を書かせて頂きたいと思います。
Sodiumとは
Hackageに公開されているSodium Packageの説明ページに拠ると、Sodiumは汎用的な Reactive Programming (FRP)システムで、複数の言語(現時点でC#,C++,Haskell,Java. http://reactiveprogramming.org/ を参照)で同じようにプログラミングできるように実装されたReactiveライブラリであり、以下の特色があるようです。
- シンプルさと完全性を目標にしている。
- EventはFunctor、BehaviourはApplicativeで実装する。(Applicativeスタイル)
- IOモナド内の好きなところから入力値を与えられる。
- SodiumとIOモナドを結合する為の多くの機能が提供されている。
- Pushベース。
・・・英語もFRPもさっぱりなので、何言ってるのか自分でもよく分かりません。でも、使うだけなら問題ないのです。たぶん。
前述のEventとBehaviorという概念はSodium(FRP)にとって重要なので、簡単な補足説明をします。具体的に例示した方が理解しやすいと思うので信号の誤り検知によく使用されるCRC(CRC-3,CRC) のブロック図を使用して説明してみます。
- dataはビット列でinから1bitづつ入力されます。
上図にあるようにEventは信号線で表現される部分(もう少し具体的にはdata in の出力,xorの出力,regの出力)、Behaviorは値を保持するレジスタ(バッファ,変数,伝達関数)に対応すると思います。
後述しますが、Sodiumにはsync関数が存在し、それは信号の取得/処理タイミングを示す同期信号(clk)のような働きをします。即ち、sync関数の実行毎にdata inから1bit入力され、レジスタの値が変化することになります。
Sodiumに用意されたツール類
ここからはもう少し具体的に見て行きましょう。Sodiumパッケージには以下の関数群が提供されています。この関数群を使ってロジックを組み立てて処理を実行することになります。上手く表現できているか自信がありませんが著者なりに分類してみました。
図中において名称の末尾に*が付加されているものはSodiumで提供されている関数です。たった24個でとてもシンプルです。
関数群を戻り値の型に基いてsync関数,Reactiveグループ,Eventグループの3つに分けてみました。
EventグループはReactiveグループに接続され、Reactiveグループはsync関数に接続されます。図中にBehaviorの名前がありませんが、Reactiveグループ内の薄い緑で着色された関数が生成してくれます。
プログラミング
Sodiumを使用する上で必要な概念は全て揃ったので、ここからは実際に作業環境を準備し、プログラムを動作させながら説明を続けたいと思います。
1. 作業環境準備:任意の作業ディレクトリ(frptest)へ移動し、パッケージをインストールします。
> cd frptest
> cabal sandbox init
> cabal install sodium
2. テストコード作成:作業ディレクトリに以下のテストコードを記述したファイルを用意します。
CRC-4を実装することを目標にテストコードを記述していきますが、まずは単純なレジスタ一つだけのロジックを記述します。
import FRP.Sodium
-- レジスタを定義(Behavior):初期値は0
reg :: Event Int -> Reactive (Behavior Int)
reg e = hold (0::Int) e
-- テスト用の関数を定義
test = do
-- ロジックを組み立て。
(inpf,r) <- sync $ do
(e,inpf) <- newEvent -- レジスタの入力端子を作成
r <- reg e -- 入力端子を渡してレジスタを作成
return (inpf,r)
-- レジスタ動作テスト。
mapM_ (run inpf r) [0..3]
where
run f r i = do
v <- sync $ do
f i -- レジスタの入力値を設定
sample r -- 現在のレジスタ値を読み取る
print v
3. 対話環境(ghci)を起動します。
> cabal repl
-- 以下 ghciモード内 --
> :l test.hs
> test
testを実行すると以下の結果が得られたと思います。
0
0
1
2
0,1,2,3という入力に対して、出力は一つ遅延して0(初期値),0,1,2となっています。
ここまでで、とてもシンプルなFRPプログラムが作成できました。
このプログラムで使用したSodiumの関数はsync,hold,newEvent,sampleの4つです。
詳細はリファレンスを参照して頂きたいのですが、大雑把に関数を説明すると以下のようになります。
- sync :: Reactive a -> IO a -- ロジック(Reactive)の作成/実行
- hold :: a -> Event a -> Reactive (Behavior a) -- Behavior(バッファ,変数)の作成
- newEvent :: Reactive (Event a, a -> Reactive ()) -- イベント端子とその発行(イベントプッシュ)関数を作成
- sample :: Behavior a -> Reactive a -- Behaviorの値を取得する
Eventが発行され、それが伝搬することで処理が実行されます。
単純すぎてかえって分かりにくいと思いますので、ここで一気にCRC-3を定義してしまいます。
{-# LANGUAGE RecursiveDo #-} -- rec構文を使用する為に必要です
import FRP.Sodium
-- レジスタを定義(Behavior):初期値は0
reg :: Event Int -> Reactive (Behavior Int)
reg e = hold (0::Int) e
-- CRC-3
crc3 :: Event Int
-> Reactive (Behavior Int, Behavior Int, Behavior Int)
crc3 dataIn = do
rec -- rec構文により帰還(フィードバック)のあるロジックを簡単に定義できます
let xor1e = snapshot xor dataIn r2 -- dataIn(入力値)とレジスタ2の値をxor
xor2e = snapshot xor xor1e r1 -- xor1eとレジスタ1の値をxor
r0 <- reg xor1e -- レジスタ0の入力値は xor1eの計算結果
r1 <- reg (getRegVal r0) -- レジスタ1の入力値は r0の値
r2 <- reg xor2e -- レジスタ2の入力値は xor2eの計算結果
return (r2,r1,r0)
where
xor a b = mod (a + b) 2 -- 排他的論理和
getRegVal r = snapshot (\ _ b -> b) dataIn r -- dataInイベントをトリガにレジスタrの値をEventに包んで発行
-- テスト用の関数を定義
test :: IO ()
test = do
-- ロジックを組み立て。
(inpf,(r2,r1,r0)) <- sync $ do
(e,inpf) <- newEvent -- レジスタの入力端子を作成
(r2,r1,r0) <- crc3 e -- CRC3のロジックを生成
return (inpf,(r2,r1,r0))
-- crc3の動作テスト。
mapM_ (sync . inpf) [1,0,1,0,1,0,1,1,1,1,0,0,1,1,0,1] -- data in(bit列): 0xAB,0xCD
crc <- mapM (sync . sample) [r2,r1,r0] -- crcの計算結果をレジスタから取り出す
print crc
上記ファイルを作業ディレクトリに作成し、ghciで動作させます。
-- 以下 ghciモード内 --
> :l crc3test.hs
> test
実行結果は[1,1,1]となったと思います。
Online CRC Calculationで結果を確認すると111 (binary)なっており、正しく動作していることが確認できます。
今回のプログラムで新たに使用したSodiumの関数はsnapshotです。
- snapshot :: (a -> b -> c) -> Event a -> Behavior b -> Event c -- Event aを受信したらBehavior bを参照し(a -> b -> c)関数の結果をEventで包んで返す(Event発行する)
また、Sodiumの機能ではありませんがrec構文:7.3.8.1. 再帰的束縛グループを使用しています。
この機能によって帰還が手軽に定義できます。
まとめ
ここまでの説明で何となくSodiumの雰囲気をお伝えできたでしょうか。今更ですがCRCの実装を例示しながら説明するよりもSodiumパッケージのリファレンスに掲載されているaccum関数の実装を説明に使用した方が分かりやすかったかもしれません。この実装はとても示唆に富むもので、数学で例えるならオイラーの公式のようにSodiumで必要とされる要素が詰め込まれています。
もしSodiumに興味を持たれて独習する際には、出発点としてこのaccum関数の実装を吟味することをお勧めします。
{-# LANGUAGE DoRec #-}
-- | Accumulate state changes given in the input event.
accum :: Context r => a -> Event r (a -> a) -> Reactive r (Behavior r a)
accum z efa = do
rec
s <- hold z $ snapshot ($) efa s
return s
以上、中途半端な紹介記事となってしまいましたが、一旦ここで区切りとします。
個々の関数についてもう少し説明したい部分もありますので日を改めて補足記事を書きたいと思います。
新しい記事を書いたらTwitterで告知しますので、宜しければフォローしてください。
最後までお読み頂きありがとうございました。