The way sized vectors are usually implemented in Haskell is with a data declaration, typically involving several fields of the same type, for example:
data V3 x = V3 !x !x !x deriving ({- ... -})
This representation has the obvious benefits of automatic deriving for many classes, but working with arbitrary sizes is rather difficult. So I’ve been toying with another approach: using a function type!
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
newtype V n x = V {unV :: Natural -> x} deriving (Functor)
This particular formulation doesn’t allow for automatic deriving of Eq, Ord, Foldable or Traversable, but with a few helper functions they are easily expressed by hand:
dimensions :: forall n. (KnownNat n) => Proxy n -> [Natural]
dimensions Proxy = case natVal (Proxy @n) of
0 -> []
n -> [0 .. pred n]
(!) :: V n x -> Natural -> x
V f ! x = f x
instance (KnownNat n, Eq x) => Eq (V n x) where
(==) :: V n x -> V n x -> Bool
v == w = List.all (\i -> v ! i == w ! i) (dimensions (Proxy @n))
instance (KnownNat n, Ord x) => Ord (V n x) where
compare :: V n x -> V n x -> Ordering
compare v w = foldMap (\i -> compare (v ! i) (w ! i)) (dimensions (Proxy @n))
instance (KnownNat n) => Foldable (V n) where
foldMap :: (Monoid m) => (a -> m) -> V n a -> m
foldMap f (V x) = foldMap (f . x) (dimensions (Proxy @n))
instance (KnownNat n) => Traversable (V n) where
traverse :: (Applicative f) => (a -> f b) -> V n a -> f (V n b)
traverse f (V x) =
traverse (f . x) (dimensions (Proxy @n)) <&> \xs ->
V \i -> xs List.!! fromIntegral i
The Traversable instance is perhaps unfortunate, since we must keep a reference to the list xs to determine the values of the resulting vector, but I haven’t heavily used the Traversable instance yet in my experiments, so I can’t muse on its performance.
The Applicative instance is also straightforward, and (<*>) in particular gives me peace:
instance (KnownNat n) => Applicative (V n) where
pure :: x -> V n x
pure x = V (const x)
(<*>) :: V n (x -> y) -> V n x -> V n y
V f <*> V x = V (f <*> x)
Of course, it’s tedious to write case expressions each time you want to define a vector, but the pattern is easily extractable:
v2 :: forall x. x -> x -> V 2 x
v2 x y = V \case
0 -> x
_ -> y
withV2 :: (x -> x -> y) -> V 2 x -> y
withV2 f v = f (v ! 0) (v ! 1)
v3 :: forall x. x -> x -> x -> V 3 x
v3 x y z = V \case
0 -> x
1 -> y
_ -> z
withV3 :: (x -> x -> x -> y) -> V 3 x -> y
withV3 f v = f (v ! 0) (v ! 1) (v ! 2)
v4 :: forall x. x -> x -> x -> x -> V 4 x
v4 x y z w = V \case
0 -> x
1 -> y
2 -> z
_ -> w
withV4 :: (x -> x -> x -> x -> y) -> V 4 x -> y
withV4 f v = f (v ! 0) (v ! 1) (v ! 2) (v ! 3)
This formulation also extends to matrices:
newtype M m n x = M {unM :: V m (V n x)} deriving (Functor)
m22 :: forall x. x -> x -> x -> x -> M 2 2 x
m22 a b c d = M $ V \i -> V \j -> case (i, j) of
(0, 0) -> a
(0, 1) -> b
(1, 0) -> c
_ -> d
{- m33, etc... -}
row :: V n x -> M 1 n x
row x = M $ V (const x)
column :: V m x -> M m 1 x
column x = M $ V \i -> V (const (x ! i))
In this case the instances map/fold/traverse over both of the dimensions, @m and @n. What’s more, the Kronecker product is super easy to exhibit:
kronecker ::
forall m n p q x.
(KnownNat p, KnownNat q, Num x) =>
M m n x -> M p q x -> M (m * p) (n * q) x
kronecker (M a) (M b) = M $ V \i -> V \j ->
let p = natVal (Proxy @p)
q = natVal (Proxy @q)
(ia, ib) = quotRem i p
(ja, jb) = quotRem j q
in a ! ia ! ja * b ! ib ! jb
Are there libraries where this kind of vector has been implemented? So far I’ve used this technique in my own attempt at Ray Tracing in One Weekend with much success, though I haven’t compared it to data libraries like linear in terms of performance.