Arrowの基本(1) >>>演算子で関数を繋ぐ
前々から気になっていたArrowを覚えるべく、あちこち資料を探してみたものの、そもそも日本語の資料が少ないので。自分で色々いじって試してみる事にしました。
一つの日本語資料として活用できるように、手順を追って何回かに別けてなるべく詳細に書いていこうと思います。
自分も勉強しながらになりますので、足りない部分ありましたら指摘いただければ幸いです。
まず、Arrowの、関数をパイプ感覚で繋いでいく(事ができるらしい)>>> 演算子の定義は次のようになってます。
Prelude Control.Arrow> :i (>>>) (>>>) :: (Control.Category.Category cat) => cat a b -> cat b c -> cat a c -- Defined in Control.Category infixr 1 >>>
ごちゃごちゃして分かりづらいですが、必要な部分だけゆっくりと汲みとっていきましょう。
ここに書かれているControl.Category.Cateogoryというのは、Control.Categoryモジュールで定義されいる型クラスです。
Arrowの話になると必ず出てくる >>> ですが、実はCategoryクラスで定義されてるんですね。
>>> が関数をつなぐ演算子であるという前提知識から、「cat a b」という記述は、cat型が(a -> b)という関数を格納するデータ型であるという事がわかります。
その事から 「cat a b -> cat b c -> cat a c」という定義は、 >>> が (a -> b)という関数と、(b -> c)という関数を合成して、(a -> c)という関数を返す演算子であると予想させてくれます。
自分はこの情報だけでは、>>>の実装を想像し切れなかったので、車輪の再発明をする事にしました。
実際に、>>> の定義を元に、同じような動作をする(であろう)MyArrowを定義しながら、(>>>)の実装を再現してみる事にしましょう。
--(a -> b)という関数を格納する data MyArrow a b = MyArrow { runArr :: (a -> b) }
ghciで実行してみます。
*Main> let myArrInc = MyArrow (\x -> x + 1) *Main> :t myArrInc myArrInc :: MyArrow Integer Integer
MyArrow が (Integer -> Integer) という関数を格納できた事が確認できました。
ラベルrunArrで、この関数を取り出せます。
*Main> :t runArr myArrInc runArr myArrInc :: Integer -> Integer *Main> runArr myArrInc $ 5 6 *Main> runArr myArrInc $ 9 10
次に、MyArrowに関数を格納するmyarr関数を定義します。
--関数をMyArrow型に格納する myarr :: (a -> b) -> MyArrow a b myarr f = MyArrow f
これは、Arrowの実際のarrに対応させたもので、関数をMyArrow型のコンテナに入れるだけなので今はあまり意味は無いですが、型クラス関数にする事でポリモーフィズムが生まれます。
そしてもう一つ、>>> に対応する --> 演算子を定義しましょう。
--関数を結合する (-->) :: MyArrow a b -> MyArrow b c -> MyArrow a c fa --> fb = myarr $ runArr fb . runArr fa
型定義を見ると、>>>の定義、cat a b -> cat b c -> cat a c とよく似ているのが解ると思います。
では、ここまで書いたコードを実際に実行してみましょう。
MyArrowから、関数そのものをrunArrで取り出し、値を適用しています。
*Main> let triInc = myarr (+1) --> myarr (+1) --> myarr (+1) *Main> let incShow = myarr (+1) --> myarr show *Main> runArr triInc $ 5 8 *Main> runArr incShow $ 5 "6"
-
- > 演算子を使って、左から右へ関数を合成している様子がわかると思います。
ここまでは単に >>> の実装を真似てみただけですが、Arrowではこれを型クラスとして定義する事で、より一般化しています。
MyArrowを型クラスにしてみましょう。
class MyArrow a where myarr :: (b -> c) -> a b c (-->) :: a b c -> a c d -> a b d runArr :: a b c -> (b -> c)
ラベルrunArrはそのままの意味で使えるよう、一旦型クラスの関数としておきます。
この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 runArr f = runFoo f --値を+1する関数をFooに包んで返す fooInc = Foo (+1)
Foo型の動作を簡潔に確認するために、(+1)をFoo型のコンテナに包んだfooInc関数を定義しておきました、実行してみましょう。
*Main> let fooDblInc = fooInc --> fooInc *Main> runArr fooDblInc $ 5 7 *Main> let fooIncShow = fooInc --> myarr show *Main> runArr fooIncShow $ 5 "6" *Main> runArr (fooInc --> fooInc --> myarr show --> myarr ("val = "++)) $ 5 "val = 7"
この例では、show関数はFoo型では無いため、myarr関数でFooに包んでいますが、これでポリモーフィズムが正しく機能している事も確認できます。
ここまでで、関数を合成していく --> 演算子はなんとなく実装できたわけですけど、Arrowのサンプルを見てみると、arr みたいなものを書かなくても普通に関数合成できてるんですね。
Prelude Control.Arrow> (+1) >>> (+1) >>> show >>> ("val = "++) $ 5 "val = 7"
これはどういう事かと思ったのですが、実はプリミティブで関数そのものを表す(->)という型が定義されていて、これをインスタンス化し拡張する事で、関数に新たな定義を追加する事ができます。
Prelude Control.Arrow> :i (->) data (->) a b -- Defined in GHC.Prim instance Monad ((->) r) -- Defined in Control.Monad.Instances instance Functor ((->) r) -- Defined in Control.Monad.Instances instance ArrowLoop (->) -- Defined in Control.Arrow instance ArrowApply (->) -- Defined in Control.Arrow instance ArrowChoice (->) -- Defined in Control.Arrow instance Arrow (->) -- Defined in Control.Arrow
この定義を見てみると、関数はArrowのインスタンスである事がわかります。
つまり、(->)に型クラスMyArrowを実装する事で、関数そのものを --> でつなぐ事できそうです。
instance MyArrow (->) where myarr f = f f --> g = g.f runArr f = f
実行結果
*Main> (+1) --> (+1) --> show --> ("val = "++) $ 5 "val = 7"
めでたく、 >>> の動作を再現する事ができました!
ここまでのメリットは、(.)の逆をやる事によって、場合によって見やすくなるだけなんですけど、MyArrowのインスタンスの定義次第では、 --> にちょっとしたからくりを仕込んだりする事ができたりします。
その他、結局どう嬉しいの?とか、Arrowって他に変な演算子色々あるよね?っていう話を、MyArrowを実際のArrowを真似て拡張しつつ、のんびり書いていこうと思います。
参考資料:
3分で解るHaskellのArrowの基本メモ
http://d.hatena.ne.jp/r-west/20070529/1180455881
Haskell/Understanding arrows
http://en.wikibooks.org/wiki/Haskell/Understanding_arrows
2011/1/11:文言等、何箇所か修正