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
そのものもものすごく強力なように思える。