Arrowの基本(2) 関数を並列に繋ぐ ***、&&& 演算子

前回からの変更点:
実際の実装に合わせて、各MyArrowクラスから関数を取り出す関数は
MyArrow型クラスに定義したrunArrでは無く、各型定義のアクセサ関数を使うようにします。

module Main where

--Arrow型クラス
class MyArrow a where
  myarr :: (b -> c) -> a b c
  (-->) :: a b c -> a c d -> a b d

--関数にMyArrowを実装
instance MyArrow (->) where
  myarr f = f
  f --> g = g.f

--MyArrowを実装したFoo型
data Foo a b = Foo { runFoo :: (a -> b) }
instance MyArrow Foo where
  myarr f = Foo f
  f --> g = Foo $ runFoo g . runFoo f

前回のエントリで、 >>>演算子を真似た -->演算子を定義する事で、Arrowが関数を繋ぐ動きを追っていきました。
今回は同じように、関数を並列に繋ぐための ***演算子、 &&&演算子の動作を再実装していきます。

Prelude Control.Arrow> :i (***)
class (Control.Category.Category a) => Arrow a where
  ...
  (***) :: a b c -> a b' c' -> a (b, b') (c, c')
  ...
  	-- Defined in Control.Arrow
infixr 3 ***
Prelude Control.Arrow> :i (&&&)
class (Control.Category.Category a) => Arrow a where
  ...
  (&&&) :: a b c -> a b c' -> a b (c, c')
  	-- Defined in Control.Arrow
infixr 3 &&&

「a b c」という型は、(b -> c)という関数を包んだArrow型クラスのインスタンス aという型を表していました。
同様に、***演算子の返り値は、( (b, b') -> (c, c') )という型の関数・・・つまりタプルで分けられた二つの値に対して、それぞれの関数( (b -> c) と (b' -> c') )を適用する関数を保持した、Arrow型のデータである事がわかります。
&&&演算子は一つの値を受け取り、それをタプルに分けてそれぞれの関数に渡す関数を返します。

つまり、
f *** g が (\(x,y) -> (f x, g y))となる関数を作るのに対して、 f &&& g は (\x -> (f x, g x)) を作るわけです。



では ***演算子を真似た *-*演算子
&&&演算子を真似た &-&演算子
を、それぞれ定義していきます。

infixr 1 -->
infixr 3 *-*
infixr 3 &-&

class MyArrow a where
  ...
  (*-*) :: a b c -> a b' c' -> a (b,b') (c,c')
  (&-&) :: a b c -> a b c' -> a b (c,c')
    • >演算子と合わせて使えるように、演算子に優先順位を設定しました。各々の型の定義はArrowの定義そのままです。

前回作ったFoo型に、これらの関数を実装すると、次のようになります

data Foo a b = Foo { runFoo :: (a -> b) }
instance MyArrow Foo where
  ...
  Foo f *-* Foo g = Foo $ \(x,y) -> (f x,g y)
  Foo f &-& Foo g = Foo $ \x -> (f x,g x)

ghciで実行してみます。

*Main> runFoo (myarr (+1) *-* myarr (+2)) $ (1,1)
(2,3)
*Main> runFoo (myarr (*2) &-& myarr (*3)) $ 2
(4,6)

実際に、左右のタプル別々の関数が適用されているのがわかりますね。
もちろん、これをさらに --> 演算子で繋ぐ事もできます。

*Main> runFoo (myarr (+1) *-* myarr (+2) --> myarr show --> myarr ("res = "++) --> myarr putStrLn) $ (1,1) 
res = (2,3)



関数を並行に繋ぐというのが、どういう事かわかった所で前回同様、関数にMyArrow型を実装してみましょう。

instance MyArrow (->) where
  ...
  f *-* g = \(x,y) -> (f x,g y)
  f &-& g = \x -> (f x,g x)

実行結果。

Main> (*2)&-&(*3) --> ("res = "++).show --> putStrLn $ 2
res = (4,6)

最後に、ここまで書いたコードが、ちゃんと本来のArrowを再現したものになっているかどうか確認して、このエントリを終わろうと思います。

Prelude Control.Arrow> (+1)***(+2) $ (1,1)
(2,3)
Prelude Control.Arrow> (*2)&&&(*3) >>> ("res = "++).show >>> putStrLn $ 2
res = (4,6)

とても直感的に関数を組み合わせていけます。Arrowすごい!