(Because you probably don't want to make Vector
a Num
, I'll be referring to monad operations instead.)
Haskell's []
monad solves this problem by simply going through every possible combination of elements. For example, [(+1),(*2)] <*> [3,6,5]
= [4,7,6, 6,12,10]
. For your Vector
implementation which only supports two or three elements, though, this won't work. Would V2 (+1) (*2) <*> V3 3 6 5
have two or three elements? For this reason, you probably want to expand your type to support all lengths of lists, which would just bring you right back to []
.
If you want to maintain this "zipping" operation, though... things get more complicated. Possible, but complicated. The basic idea is to embed the length of the vector into the type while still maintaining polymorphism. We can essentially do this with type-level natural numbers...
data N = Z | S N
(Z
stands for "zero" and S
for "successor", e.g. S (S (S (S Z)))
= 4)
...and we can utilize this with data kinds and GADTs:
data Vector n a where
Nil :: Vector 'Z a
(:^) :: a -> Vector n a -> Vector ('S n) a
Here, n
is the length of the vector. For example, V2
would be represented by Vector (S (S Z)) a
and V3
by Vector (S (S (S Z))) a
. Notably, though, you can make the length polymorphic, e.g. Vector (S n) a
is the type of all non-empty vectors (length is at least one). Unfortunately, (GHC's) type inference breaks down in the presence of GADTs, so you'll probably need scoped type variables (and a lot of explicit type signatures) to do anything useful.
All the actual data definition is saying is that Nil
has length zero, and an element prepended to a vector (x :^ xs
) has length one more than the vector given.
As an example, here's a Functor
instance, defining fmap
analogously to []
's map
:
instance Functor (Vector n) where
fmap :: forall a b. (a -> b) -> Vector n a -> Vector n b
fmap f = go where
go :: Vector k a -> Vector k b
go Nil = Nil
go (x:^xs) = f x :^ go xs
Notice all of the type signatures littered around. Unfortunately, this is almost always necessary because GHC isn't too keen to assume polymorphism in the presence of GADTs. The forall
here helps scope a
and b
so that they can be used in the type for go
. If it wasn't there, then the a
and b
in fmap
and the a
and b
in go
would be completely separate type variables, just with similar names.
To define more interesting functions, GHC needs to know that we want to interact with the length of the list, so we define a singleton type which "refines" the length...
data P n where
PZ :: P 'Z
PS :: P n -> P ('S n)
(read P n
as "a proof of n
's natural value)
...and a class which gives access to that type:
class Nat n where
nat :: P n
instance Nat 'Z where
nat = PZ
instance Nat n => Nat (S n) where
nat = PS nat
From here, we can define replicateV
analogously to []
's replicate
:
replicateV :: forall n a. Nat n => a -> Vector n a
replicateV x = go nat where
-- read as "go takes a proof of k's value and returns a vector of length k"
go :: P k -> Vector k a
go PZ = Nil
go (PS p) = x :^ go p
From here, you can define a vzip :: (a -> b -> c) -> Vector n a -> Vector n b -> Vector n c
similar to []
's zip
.
(See also this question which expands on these ideas in a slightly different way.)