言語ゲーム

とあるエンジニアが嘘ばかり書く日記

Twitter: @propella

[haskell] How To Arrow2 並置

import Control.Arrow
newtype SF a b = SF {runSF ::[a]->[b]}
instance Arrow SF where
    arr f = SF (map f)
    SF f >>> SF g = SF (f >>> g)
    first (SF f) = SF (unzip >>> first f >>> uncurry zip)

-- ではややこしい話の前に、Arrow の利点を強調する例。向こうからやって
-- 来た数字の値を二倍したい。やってくるのは数字と分かっているが、数字
-- は一つかも知れないし、リストに入っているかも知れないし、IO 操作に関
-- する物かも知れない。なんと Arrow を使うと、同じ定義が、リストにもス
-- カラにも(多分モナドにも)使える!ここ感動する所です!

double :: (Arrow a, Num b) => a b b
double = arr (2 *)
-- *Main> double 7
-- 14
-- *Main> runSF double [1..5]
-- [2,4,6,8,10]

-- 完全に余談だが、何故ここが感動する所かを書く。オブジェクト指向プロ
-- グラミングで、クラスの違うオブジェクトに同じメソッドを使いたい場合
-- がある。Smalltalk 等の動的言語では同じ名前のメソッドを定義するだけ
-- 良くて、例えば Collection オブジェクトは APL の影響を受けて + や * 
-- 等の演算子で数値の配列を纏めて計算出来る。また、スプレッドシートの
-- ような物を実装したくて、セルA + セルB 等とした時に + の意味するのは
-- セルの(セル自体ではなく)内容を足すという意味にしたい場合があるだろ
-- う。問題はいくつかある。1) 実行時型エラーの恐怖、2) 同じようなメソッ
-- ドをあちこち実装しないといけない。3) 沢山演算を組み合わせると効率が
-- 落ちる。それを Arrow は無茶苦茶スマートに解決するのだ!1) 2) は言わ
-- ずもがな。3) に関しては分配法則が使える(arr double >>> arr double) 
-- は arr (double >>> double) と書ける)。超凄い。夢のようだ。

-- さて、馬鹿馬鹿しい例だけど、おなじみ華氏摂氏変換器を考える。公式は 
-- 華氏 = 摂氏 * 1.8 + 32 です。

fahrenheit c = c * 1.8 + 32

-- 摂氏と華氏を並べた表を、データフロー的に作る。演算子 &&& は Arrow 
-- を二つに分岐させる。結果として二本の Arrow がタプルで結びついた形に
-- なる。この例では結びついた Arrow をそのまま結果として返す(心の声: 
-- これくらい自分で型推論してくれないと面倒で堪らない!)。

fcTable :: Arrow a => a Float (Float, Float)
fcTable = arr fahrenheit &&& arr id
-- *Main> runSF fcTable [0, 10, 20, 30]
-- [(32.0,0.0),(50.0,10.0),(68.0,20.0),(86.0,30.0)]

-- &&& は一本の Arrow を二つに分岐するが、最初から分岐している Arrow 
-- を処理する部品を書きたい場合には *** を使う。>>> が Arrow を直列に
-- 繋げるのに対して、*** は Arrow を並列に繋げる。華氏と摂氏の表に単位
-- をつけてみる。

printFC :: Arrow a => a (Float, Float) (String, String)
printFC = arr (\x -> show x ++ " F") *** arr (\x -> show x ++ " C")
-- *Main> printFC (32, 0)
-- ("32 F","0 C")

-- というわけで二つを繋げる。fcTable が二本の Arrow を返して、printFC 
-- は二本の Arrow を受け取るのでちゃんと繋がる

printFCTable :: Arrow a => a Float (String, String)
printFCTable = fcTable >>> printFC
-- *Main> runSF printFCTable [0, 10, 20, 30]
-- [("32.0 F","0.0 C"),("50.0 F","10.0 C"),("68.0 F","20.0 C"),("86.0 F","30.0 C")]

-- 自分で分岐するコードを書けば *** を使って&&& が定義出来る。さらに原
-- 始的な関数として first が定義されている。first は二本の Arrow のう
-- ち、最初の Arrow の処理だけ記述する。例えば fcTable は次のように書
-- ける。

fcTable' :: Arrow a => a Float (Float, Float)
fcTable' = arr (\a -> (a,a)) >>> first (arr fahrenheit)

-- コツ: 頻出する Arrow a => a Float (Float, Float) のような型定義だが、
-- 次のように考えると分かりやすい。Arrow a => a の部分は当たり前なので
-- 読まない。次の Float (Float, Float) だけが肝心。Float 入りの何かが
-- やってきて、Float 二本が出て行く。関数 Float -> (Float, Float) の特
-- 別な場合と考えても良い。

-- さらに完全な余談第二弾だが。何故 Arrow をやってるかと言うと、Arrow 
-- は型付の Joy であるというのが私の予想。また、Arrow をスタック言語と
-- みなす事によって超強力かつさらにシンプルになるのでは無いかと考えて
-- いる。