2009-06-12
ベクトルの足し算をControl.Applicativeで
data Vector2 a = Vector2 a a deriving (Show, Eq)
こういう型をNumのinstanceにすることを考える。単純に書くとこうだ。
instance Num a => Num (Vector2 a) where Vector2 x y + Vector2 x' y' = Vector2 (x + x') (y + y') Vector2 x y - Vector2 x' y' = Vector2 (x - x') (y - y') Vector2 x y * Vector2 x' y' = Vector2 (x * x') (y * y') negate (Vector2 x y) = Vector2 (-x) (-y) abs (Vector2 x y) = Vector2 (abs x) (abs y) signum (Vector2 x y) = Vector2 (signum x) (signum y) fromInteger x = Vector2 (fromInteger x) (fromInteger x)
掛け算などの定義には突っ込まないでほしい。僕はVector 1 2 + Vector 3 4と書きたいだけなのだ。
上だけなら対した量ではないが、Vector3やVector4を実装するとなると、コピぺしていじる作業が辛くなってくる。
この状況を改善したい。
fmapを使う
fmapを使うとすこし楽になる。
instance Functor Vector2 where fmap f (Vector2 x y) = Vector2 (f x) (f y) instance Num a => Num (Vector2 a) where Vector2 x y + Vector2 x' y' = Vector2 (x + x') (y + y') Vector2 x y - Vector2 x' y' = Vector2 (x - x') (y - y') Vector2 x y * Vector2 x' y' = Vector2 (x * x') (y * y') negate = fmap negate abs = fmap abs signum = fmap signum fromInteger x = fmap fromInteger (Vector2 x x)
とはいえ、足し算や引き算は変わらない。
fmap2がほしい
パターンはこうだ。
Vector2 x y `op` Vector2 x' y' = Vector2 (x `op` x') (y `op` y')
そのままこれを関数とするとこうなる。
op :: (a -> b -> c) -> Vector2 a -> Vector2 b -> Vector2 c op f (Vector2 x y) (Vector2 x' y') = Vector2 (x `f` x') (y `f` y')
これを使うとこう定義できる。
instance Num a => Num (Vector2 a) where (+) = op (+) (-) = op (-) (*) = op (*) negate = fmap negate abs = fmap abs signum = fmap signum fromInteger x = fmap fromInteger (Vector2 x x)
opはfmapの二引数関数用と言ってもいい。こういうクラスと関数があってもいいはずだ。
class Functor2 f where fmap2 :: (a -> b -> c) -> f a -> f b -> f c -- で instance Functor2 Vector2 where fmap2 = op instance Num a => Num (Vector2 a) where (+) = fmap2 (+) (-) = fmap2 (-) (*) = fmap2 (*) negate = fmap negate abs = fmap abs signum = fmap signum fromInteger x = fmap fromInteger (Vector2 x x)
fmap2は標準に含まれていない。
liftA2
fmap2は無いが、幸いなことにControl.Applicativeで用が足りる。
liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
つまり、Vector2をApplicativeのinstanceにすればこの関数が使える。
instance Applicative Vector2 where pure x = Vector2 x x Vector2 fx fy <*> Vector2 x y = Vector2 (fx x) (fy y) instance Functor Vector2 where fmap = liftA instance Num a => Num (Vector2 a) where (+) = liftA2 (+) (-) = liftA2 (-) (*) = liftA2 (*) negate = fmap negate abs = fmap abs signum = fmap signum fromInteger = pure . fromInteger
これで重複が少なくなり、コピペしやすいコードになった。(ついでにfromIntegerもpureで書き換えた)
liftA2はFractionalの実装にもそのまま使える。
instance (Fractional a) => Fractional (Vector2 a) where (/) = liftA2 (/) recip = fmap recip fromRational = pure . fromRational
副産物
ここまで実装すると不思議なことが起こる。Vectorが割れるのだ。
GHCi, version 6.10.2: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done. Loading package integer ... linking ... done. Loading package base ... linking ... done. [1 of 1] Compiling Main ( va.hs, interpreted ) Ok, modules loaded: Main. *Main> Vector2 3 4 / 5 Vector2 0.6 0.8 *Main> Vector2 3 4 * 3 Vector2 9 12
これは数字リテラルがオーバーロードされていることによる。
*Main> :t 5 5 :: (Num t) => t *Main> 5 :: Vector2 Int Vector2 5 5
こちらはすこし気持ち悪いかもしれない。
*Main> Vector2 1.5 2 + (-3.2) Vector2 (-1.7000000000000002) (-1.2000000000000002) *Main> Vector2 (-3) 1.9 - 4 Vector2 (-7.0) (-2.1)
コメントを書く
トラックバック - http://haskell.g.hatena.ne.jp/illillli/20090612