[Haskell-cafe] Categorical description for vector-space instance of AdditiveGroup (Maybe a)

Jason McCarty jmccarty at sent.com
Wed Sep 17 04:20:46 UTC 2014


Warning: the following post contains graphic abstract algebra and
category theory, and may not be suitable for all viewers. This post is
literate Haskell.

I want to revisit our bogus instances for Maybe v and provide some
partial resolutions. The IMO best one is given by an operator (*.^) near
the end.

I'll abbreviate the categories AdditiveSemigroup, AdditiveMonoid,
AdditiveGroup as S, M, G.

The package vector-space has the instance

  instance AdditiveGroup v => AdditiveGroup (Maybe v)

which we found was bogus because Just zeroV /= Nothing, so Maybe v
doesn't have well-defined negation.

Let's pretend the class hierarchy is the following.

> {-# LANGUAGE FlexibleContexts, FlexibleInstances, TypeFamilies #-}
> module Maybe where
> import Control.Monad (join)

> class AdditiveSemigroup a where
>   (^+^) :: a -> a -> a -- commutative, associative operator

> class AdditiveSemigroup a => AdditiveMonoid a where
>   zeroV :: a -- unit for (^+^)

> class AdditiveMonoid a => AdditiveGroup a where
>   negateV :: a -> a -- inverse for (^+^) and zeroV

> class Semigroup a where
>   (^*^) :: a -> a -> a -- associative operator

> class Semigroup a => Monoid a where
>   oneV :: a -- unit for (^*^)

> class (AdditiveGroup a, Monoid a) => Ring a
> -- (^*^) distributes over (^+^)

> class (AdditiveMonoid a, Monoid a) => Semiring a
> -- (^*^) distributes over (^+^) and zeroV ^*^ x == zeroV == x ^*^ zeroV

> class (AdditiveSemigroup a, Monoid a) => WeakSemiring a
> -- (^*^) distributes over (^+^)

We can give Hom_C(u, v) some algebraic structure depending on the
category C.

> -- represents Hom_C(u, v) for any concrete category C, which is
> -- unfortunate but too annoying to fix here
> type Hom u v = u -> v
> type End v = Hom v v -- represents End_C(v) = Hom_C(v, v)

Morphisms can be added pointwise:

> instance AdditiveSemigroup v => AdditiveSemigroup (Hom u v) where
>   f ^+^ g = \x -> f x ^+^ g x

> instance AdditiveMonoid v => AdditiveMonoid (Hom u v) where
>   zeroV = const zeroV

> instance AdditiveGroup v => AdditiveGroup (Hom u v) where
>   negateV f = negateV . f

We can view End_C(v) as a monoid with composition:

> instance Semigroup (End v) where
>   f ^*^ g = f . g

> instance Monoid (End v) where
>   oneV = id

(^*^) distributes over (^+^) in End_C(v):

> instance AdditiveSemigroup v => WeakSemiring (End v)

> instance AdditiveMonoid v => Semiring (End v)
>   -- zeroV ^*^ y = zeroV = x ^*^ zeroV too

> instance AdditiveGroup v => Ring (End v)

With this in place, if c, d are objects in categories C, D, then an
action of c on d can be viewed as an element of Hom_C(c, End_D(d)),
provided End_D(d) can be viewed as an object in C. In particular, a
field f acts on a vector space v by a ring morphism f -> End_G(v). For
the purpose of this discussion, we ignore multiplicative inverses; our
scalars can be any ring.

> class Action v where
>   type Scalar v
>   (*^) :: Scalar v -> End v

> class (Action v, Ring (Scalar v), AdditiveGroup v) => VectorSpace v
>   -- (*^) must be a ring morphism

Our structure on Maybe v is:

> instance AdditiveSemigroup v => AdditiveSemigroup (Maybe v) where
>   x ^+^ Nothing = x
>   Nothing ^+^ y = y
>   Just x ^+^ Just y = Just $ x ^+^ y

> instance AdditiveSemigroup v => AdditiveMonoid (Maybe v) where
>   zeroV = Nothing

Since Maybe v isn't a group, we can't define an instance
VectorSpace (Maybe v). But Maybe v is a monoid, so End_M(Maybe v) is a
semiring. So I figured I could just weaken vector spaces a little:

> class (Action v, Semiring (Scalar v), AdditiveMonoid v) => SemiVectorSpace v
>   -- (*^) must be a semiring morphism

I guessed that fmap :: End_M(v) -> End_M(Maybe v) would be a semiring
morphism, which gives:

> instance Action v => Action (Maybe v) where
>   type Scalar (Maybe v) = Scalar v
>   (*^) = fmap . (*^)

  instance SemiVectorSpace v => SemiVectorSpace (Maybe v)
    -- (*^) = fmap . (*^) is a composite of semiring morphisms?

I was wrong about fmap: although fmap preserves (^+^), (^*^), and oneV,
the Just zeroV /= Nothing problem strikes again! We have
fmap (zeroV :: End_M(v)) = fmap (const zeroV), but
(zeroV :: End_M(Maybe v)) = const Nothing.

I should have known better: fmap actually has type
End_S(v) -> End_M(Maybe v), so there's no reason to expect it to
preserve zeroV, even if v happens to have it.

That forces you to weaken things even further:

> class (Action v, WeakSemiring (Scalar v), AdditiveSemigroup v) => WeakSemiVectorSpace v
>   -- (*^) must be a weaksemiring morphism

> instance WeakSemiVectorSpace v => WeakSemiVectorSpace (Maybe v)
> -- (*^) = fmap . (*^) is a composite of weaksemiring morphisms

So this is one "solution," but to me not very satisfying.

Then I thought that, since Maybe is a monad, perhaps Hom_M(v, Maybe v)
and/or Hom_S(v, Maybe v) are monoids under Kleisli composition. Are
these semigroups under Kleisli composition?

> instance Semigroup (Hom v (Maybe v)) where
>   f ^*^ g = join . fmap f . g -- = \x -> g x >>= f

Here join is a semigroup morphism and fmap f is a monoid morphism if f
is a semigroup morphism. In fact, join (zeroV :: Maybe (Maybe v))
== zeroV :: Maybe v, so join is also a monoid morphism. Hence f ^*^ g is
a composite of appropriate morphisms, making Hom_M(v, Maybe v) and
Hom_S(v, Maybe v) semigroups. 

To make this a monoid, we'd have to have oneV = Just, which is a
semigroup morphism, but not a monoid morphism.

> instance Monoid (Hom v (Maybe v)) where
>   oneV = return -- only valid for Hom_S(v, Maybe v)!

This also makes (^*^) distribute over (^+^), so Hom_S(v, Maybe v) is a
semiring if v is an additive semigroup, and Hom_M(v, Maybe v) is a
semiring without one if v is an additive monoid (this is called a
semirng because the "i" in "ring" supposedly stands for "multiplicative
identity!").

> instance AdditiveSemigroup v => Semiring (Hom v (Maybe v))
>   -- only valid for Hom_S(v, Maybe v)

> class (AdditiveMonoid v, Semigroup v) => Semirng v
>   -- (^*^) distributes over (^+^)

> instance AdditiveMonoid v => Semirng (Hom v (Maybe v))
>   -- valid for Hom_M(v, Maybe v)

Now we can hypothetically define a semiring morphism
f -> Hom_S(v, Maybe v) if v is a SemiVectorSpace over f; we just need a
semiring morphism End_M(v) -> Hom_S(v, Maybe v).

> kleisli :: SemiVectorSpace v => (End v -> Hom v (Maybe v)) -> Scalar v
>   -> Hom v (Maybe v)
> kleisli mor = mor . (*^)

This would also define a semirng morphism f -> Hom_M(v, Maybe v) from a
semirng morphism End_M(v) -> Hom_M(v, Maybe v).

Only problem: I can't think of any natual semiring morphism
End_M(v) -> Hom_S(v, Maybe v) or semirng morphism
End_M(v) -> Hom_M(v, Maybe v), except for const (const Nothing).
Perhaps this would be useful if you have such a map handy, but not in
general. If you only want a weaksemir(i)ng morphism, you can use
mor f = Just . f, but this is no better than the WeakVectorSpace
instance above.

What about the CoKleisli category? Maybe isn't just a monad on S, it's
also a comonad on M! In fact, let's look at arbitrary comonads on M.

> class Functor m => MonoidComonad m where
>   coreturn :: AdditiveMonoid v => Hom (m v) v
>   cojoin :: AdditiveMonoid v => Hom (m v) (m (m v))
>   -- cojoin and coreturn must be monoid morphisms
>   -- fmap must take monoid morphisms to monoid morphisms
>   -- cojoin, coreturn, and fmap must satisfy the comonad laws

Hopefully Hom_M(m v, v) is a monoid under CoKleisli composition.

> instance (MonoidComonad m, AdditiveMonoid v)
>   => Semigroup (Hom (m v) v) where
>      f ^*^ g = f . fmap g . cojoin

This is a composite of monoid morphisms if f and g are. The map coreturn
is also a monoid morphism, so Hom_M(m v, v) is a monoid.

> instance (MonoidComonad m, AdditiveMonoid v)
>   => Monoid (Hom (m v) v) where
>      oneV = coreturn

This also implies that (^*^) distributes over (^+^), so I would expect
this to be a semiring. It's easy to check that zeroV ^*^ g == zeroV, but
f ^*^ zeroV doesn't necessarily equal zeroV when v is not cancellative
(m = Maybe and v = Bool with (^+^) = (||) is a counterexample). So this
isn't necessarily a semiring when v is just a monoid.

However, all groups are cancellative, so we get the following better
result.

> instance (MonoidComonad m, AdditiveGroup v) => Ring (Hom (m v) v)

Hom_M(m v, v) is a ring when v is an additive group! Finally, we can get
a ring morphism f -> Hom_M(m v, v) if v is a vector space over f, given
a ring morphism End_G(v) -> Hom_M(m v, v).

> coKleisliWith :: (MonoidComonad m, VectorSpace v)
>   => (End v -> Hom (m v) v) -> Scalar v -> Hom (m v) v
> coKleisliWith mor = mor . (*^)

The obvious candidate for such a morphism is mor f = f . coreturn, and
it's easy to see that mor preserves zeroV and oneV. Moreover,

mor (f ^+^ g) = (f ^+^ g) . coreturn
              = \x -> (f ^+^ g) (coreturn x)
              = \x -> f (coreturn x) ^+^ g (coreturn x)
              = f . coreturn ^+^ g . coreturn
              = mor f ^+^ mor g

mor f ^*^ mor g = (f . coreturn) ^*^ (g . coreturn)
                = f . coreturn . fmap (g . coreturn) . cojoin
                = f . g . coreturn . coreturn . cojoin
                = f . g . coreturn
                = (f ^*^ g) . coreturn
                = mor (f ^*^ g)

So mor also preserves (^+^) and (^*^), making it a ring morphism. Let's
give the result a slick name.

> (*.^) :: (MonoidComonad m, VectorSpace v) => Scalar v -> Hom (m v) v
> (*.^) = coKleisliWith (. coreturn)

Actually, this simplifies a lot:
  s *.^ x = s *^ coreturn x

Since (*.^) is a ring morphism, this satisfies a modified version of the
vector space laws:
  1. oneV *.^ x        = coreturn x
  2. (s1 ^*^ s2) *.^ x = ((*.^) s1 ^*^ (*.^) s2) x
                       = s1 *.^ ((s2 *^) <$> x)
  3. s *.^ (x ^+^ y)   = (s *.^ x) ^+^ (s *.^ y)
  4. (s1 ^+^ s2) *.^ x = (s1 *.^ x) ^+^ (s2 *.^ x)

So what comonads are there on M? Any adjunction (F: C -> M, G: M -> C)
gives rise to a comonad FG: M -> M. The Maybe comonad comes from an
adjunction of semigroups and monoids, where F adds a new zeroV, and G
forgets that a monoid has a zeroV.

> instance MonoidComonad Maybe where
>   coreturn = maybe zeroV id
>   cojoin = fmap return

It seems reasonable to use lists as well, since we all know [v] is the
free monoid on v, giving an adjunction between Haskell types and
monoids. But [v] isn't commutative, you say. It doesn't matter. We can
just use Hom_M'([v], v) instead, where M' is the category of all
monoids. Everything else stays the same.

> instance MonoidComonad [] where
>   coreturn = foldr (^+^) zeroV
>   cojoin = fmap return

We can just as easily use a comonad on G or G', the category of all
groups. For example, the free group functor, the free abelian group
functor, the free abelian monoid functor, and tensoring with a fixed
abelian group are all left adjoints with codomain G', G, M, and G,
respectively, so they give rise to comonads on these categories.
Although, to implement these in Haskell, I think you need an Ord/Eq
constraint (you might get away without it if you restrict to finite-
dimensional vector spaces over finite fields, in which case you also get
the free vector space functor). Abelianization is also a left adjoint,
but it's the identity on vector spaces, so that's rather pointless.
There are probably lots of other examples.

I suspect you could get a monadic version with Kleisli composition to
work too. The problem with Maybe was that it's a monad on S, not M.

In summary, the (*.^) operator is the most flexible and principled
approach I can think of to treat Maybe v like a vector space. Is it
useful? I don't know!

-- 
Jason McCarty <jmccarty at sent.com>


More information about the Haskell-Cafe mailing list