TRANSPOSITION FOR FIXED-SIZED MATRICES, TAKE TWO Bruno Oliveira and Jeremy Gibbons 5th December 2005 Bruno was rightly dissatisfied with my approach, as outlined in "Transposition for Fixed-Sized Matrices". He showed me a better way, which I write up here. The programs are his (though I understand that they owe something to Conor's lecture notes on Epigram from AFP2005); the explanation is mine. Again, we use GADTs from GHC 6.4+ > {-# OPTIONS -fglasgow-exts #-} and again, we make use of type-level naturals as length indexes for vectors: > data Z > data S n > data Vector n a where > VNil :: Vector Z a > VCons :: a -> Vector n a -> Vector (S n) a > toList :: Vector n a -> [a] > toList VNil = [] > toList (VCons a x) = a : toList x > instance Show a => Show (Vector n a) where > show = show . toList Type-level naturals have value-level representatives: > data Nat n where > Z :: Nat Z > S :: Nat n -> Nat (S n) > len :: Vector n a -> Nat n > len VNil = Z > len (VCons a x) = S (len x) Here are two simple combinators on vectors: "rep" makes a constant vector of a given length, and "zap" is "zip with apply". Note that the two arguments of zap are forced at the type level to be of the same length. > vrep :: Nat n -> a -> Vector n a > vrep Z x = VNil > vrep (S n) x = VCons x (vrep n x) > zap :: Vector n (a -> b) -> Vector n a -> Vector n b > zap VNil VNil = VNil > zap (VCons f fs) (VCons x xs) = VCons (f x) (zap fs xs) With vrep and zap, we can build vmap and vfork as before (although they aren't used in this script): > vmap :: (a->b) -> Vector n a -> Vector n b > vmap f x = zap (vrep (len x) f) x > vfork :: Vector n (a->b) -> a -> Vector n b > vfork fs a = zap fs (vrep (len fs) a) Transposition is much easier to write if we explicitly pass the sizes as parameters, rather than deducing them from the given matrix. In particular, there is no longer a problem in computing the width of an empty matrix. > tr :: Nat n -> Nat m -> Vector n (Vector m a) -> Vector m (Vector n a) > tr Z m VNil = vrep m VNil > tr (S n) m (VCons x xs) = zap (zap (vrep m VCons) x) (tr n m xs) Now we expect that tr n m . tr m n = id > v1 = VCons 1 (VCons 2 (VCons 3 VNil)) > v2 = VCons 4 (VCons 5 (VCons 6 VNil)) > v3 = VCons 7 (VCons 8 (VCons 9 VNil)) > three = S (S (S Z)) > m1 = VCons v1 (VCons v2 (VCons v3 VNil)) > proofTest n m = tr n m . tr m n > proofTest' = proofTest three three m1 More interesting, perhaps, is the observation that fixed-length vectors form what Conor McBride calls an "idiom" (or, to use the revised name, an "applicative functor"). > class Functor f => Applicative f where > pure :: a -> f a > (<*>) :: f (a -> b) -> f a -> f b Applicative functors are partway between functors and monads; every monad is an applicative functor (with return for pure, and ap for <*>), and every applicative functor is a functor. McBride and Paterson motivate an applicative functor f as a type constructor that embeds the usual notion of value, but has "its own peculiar way of giving meaning to the usual applicative language - its idiom". As well those of functors, applicative functors must satisfy the following laws: pure id <*> u = u pure (.) <*> u <*> v <*> w = u <*> (v <*> w) pure f <*> pure x = pure (f x) u <*> pure x = pure ($ x) <*> u Conor and Ross's paper shows that the ordinary list functor is an applicative functor: > instance Applicative [] where > pure = repeat > (<*>) = zipWith ($) It turns out that so is the functor Vector n, for arbitrary size n. (In fact, Vector n is a monad, too, as are infinite lists; whereas lists with repeat and zipWith ($) are not a monad - ragged lists break the law.) To install Vector n as an instance of Applicative, however, we cannot just use the operations vrep and zap defined above, because vrep has the wrong type: the length is passed explicitly. Instead, we need to infer that length by type, for which we need type classes. > class Size n where > rep :: a -> Vector n a > instance Size Z where > rep x = VNil > instance Size n => Size (S n) where > rep x = VCons x (rep x) > instance Functor (Vector n) where > fmap = vmap > instance Size n => Applicative (Vector n) where > pure = rep > (<*>) = zap This doesn't help us much with our transposition. However, note that the definition given above is constructed entirely out of applicative functor widgets, so we can now write instead: > tr2 :: Size m => Vector n (Vector m a) -> Vector m (Vector n a) > tr2 VNil = pure VNil > tr2 (VCons x xs) = pure VCons <*> x <*> tr2 xs This suggests that there is more to transposition than what we've seen about vectors: the same definition works for any applicative functor. So here is a more generic version of tr: > gtr :: Applicative f => Vector n (f a) -> f (Vector n a) > gtr VNil = pure VNil > gtr (VCons x xs) = pure VCons <*> x <*> gtr xs Conor and Ross take this further. You might see that both equations for gtr simply replicate the vector structure, reinterpreting constructor applications in the idiom. This same idea works for any datatype functor (certainly, regular functors; probably, anything that corresponds to a relator). Moreover, it fuses with map; so the construction can be generalized a little further, by applying some function idiomatically to the elements of the data structure. > class Traversable t where > traverse :: Applicative f => (a -> f b) -> t a -> f (t b) > instance Traversable (Vector n) where > traverse f VNil = pure VNil > traverse f (VCons x xs) = pure VCons <*> f x <*> traverse f xs If we specialize so that the function applied everywhere is just the identity function, we get what Conor and Ross call an "applicative distributor" (I believe this is what Lambert Meertens called "functor pulling"): > dist :: (Applicative f, Traversable t) => t (f b) -> f (t b) > dist = traverse id Applicative distribution is a generic zip or commuter for datatypes; specialized to vectors, it is of course our transposition. > tr3 :: Size m => Vector n (Vector m a) -> Vector m (Vector n a) > tr3 = dist I still haven't tried out the proof that tr is its own inverse, though.