Hatena::Grouphaskell

[ pred x | x <- "Ibtlfmm!ojllj" ] RSSフィード

2009-06-12

ベクトルの足し算をControl.Applicativeで 01:12

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

これで重複が少なくなり、コピペしやすいコードになった。(ついでにfromIntegerpureで書き換えた)

liftA2Fractionalの実装にもそのまま使える。

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