Haskell の Functor
について調べてたらこんなのを見つけた。
stackoverflow.com
ここで挙げられているエクササイズを見てみると Functor
の深さにめまいがしてちょっとだけウンコを漏らした。
2問目
Functor
の composition をデータ型で定義しろという問題で、実装はこうなる。
{-# LANGUAGE TypeOperators #-} newtype (:.) f g x = Comp {comp :: f (g x)} instance (Functor f, Functor g) => Functor (f :. g) where fmap f (Comp x) = Comp $ fmap (fmap f) x instance (Applicative f, Applicative g) => Applicative (f :. g) where pure = Comp . pure . pure Comp f <*> Comp x = Comp $ fmap (<*>) f <*> x instance (Foldable f, Foldable g) => Foldable (f :. g) where foldMap f (Comp x) = foldMap (foldMap f) x instance (Traversable f, Traversable g) => Traversable (f :. g) where traverse f (Comp x) = Comp <$> traverse (traverse f) x
Traversable
や Foldable
な値はこれまでに扱ったことはあるが自前のデータ型に実装するのは初めてだったのでよくわからんという感じになりながらも定義した。
特に f (g x)
を掘っていくという感覚になれるのがちょっとてこずったが、一度感覚をつかめばまあこうなるかという感じでうまく言語化するには多分圏論の知識が必要なんだと思う。
それはいいとして、Comp
を定義することによって以下のような使い方ができる。
data Triple a = Tr a a a instance Functor Triple where fmap f (Tr a b c) = Tr (f a) (f b) (f c) instance Applicative Triple where pure a = Tr a a a Tr f g h <*> Tr a b c = Tr (f a) (g b) (h c) instance Foldable Triple where foldMap f (Tr a b c) = mconcat [f a, f b, f c] instance Traversable Triple where traverse f (Tr a b c) = Tr <$> f a <*> f b <*> f c type Zone = Triple :. Triple type Board = Zone :. Zone
Triple
は3要素のタプルとみなせるので Zone
は 3x3 の配列で Board
9 x 9 の配列と解釈することができる。これを実現するのが Comp
であり、その奥には Functor
の存在がある。
ところで、Zone
を traverse
すると何が起きるのか把握しておきたい。
ghci> z = Comp (Tr (Tr 1 2 3) (Tr 4 5 6) (Tr 7 8 9)) :: Zone Int ghci> traverse (Just . (+1)) z Just (Comp {comp = Tr (Tr 2 3 4) (Tr 5 6 7) (Tr 8 9 10)})
なるほど!
というわけで、Functor
の composition と Traversable
の組み合わせは非常に強力だということが分かった。
5問目
設問としては
(5) Consider the constant functors
であって、実装含めたのがこれ
newtype K a x = K {unK :: a} instance (Monoid a) => Functor (K a) where fmap _ (K a) = K a instance (Monoid a) => Applicative (K a) where pure _ = K mempty K a <*> K b = K $ a <> b crush :: (Traversable f, Monoid b) => (a -> b) -> f a -> b crush f t = unK $ traverse (K . f) t
newtype K a x
は設問にある通り constant な Functor
であるがゆえに fmap
の実装は K a
をそのまま返していて、Applicative
の実装をするにあたっては Monoid
であるという型制約を考慮した実装をしていて特段難しいことは無い。がゆえに K
がどういう意図のデータ型なのかあまりわからなかった。(Monoid
のラッパーであるようにしか見えない)
crush
における K
の使い方を見てみると Functor
スゲーとなる。この関数が何をしているかと言えばシグネチャのまんまではあるけど Traversable
な値を Monoid
に変換してその値を unwrap するということになるので、順次 traverse
した結果を取り出すということになるのでこのように使える。
ghci> import Data.Monoid (Sum(Sum)) ghci> crush (Sum . (+10)) [1, 2, 3] :: Sum Int Sum {getSum = 36}
これの何が利点なのかと言えば、とあるデータ型について閉じた演算が定義されているので値を直接こねくり回すことなくとても簡潔に関数を定義できることにある。(はず。Haskell で実現しているのはそういうもんのはず。)
同エクササイズで all
と any
を定義しろというのだけどもこれも以下の通りデータ型を実装できる。
import Prelude hiding (all, any) newtype Any = Any {unAny :: Bool} instance Semigroup Any where (Any x) <> (Any y) = Any $ x || y instance Monoid Any where mempty = Any False newtype All = All {unAll :: Bool} instance Semigroup All where (All x) <> (All y) = All $ x && y instance Monoid All where mempty = All False any :: (Traversable t) => (a -> Bool) -> t a -> Bool any p = unAny . crush (Any . p) all :: (Traversable t) => (a -> Bool) -> t a -> Bool all p = unAll . crush (All . p)
こう見ると Traversable
そのものもものすごく強力なように思える。