简体   繁体   中英

Are monads Writer m and Either e categorically dual?

I noticed there is a dual relation between Writer m and Either e monads. If m is a monoid, then

unit :: () -> m
join :: (m,m) -> m

can be used to form a monad:

return is composition: a -> ((),a) -> (m,a)
join is composition: (m,(m,a)) -> ((m,m),a) -> (m,a)

The dual of () is Void (empty type), the dual of product is coproduct. Every type e can be given "comonoid" structure:

unit :: Void -> e
join :: Either e e -> e

in the obvious way. Now,

return is composition: a -> Either Void a -> Either e a
join is composition: Either e (Either e a) -> Either (Either e e) a -> Either e a

and this is the Either e monad. The arrows follow exactly the same pattern.

Question: Is it possible to write a single generic code that will be able to perform both as Either e and as Writer m depending on the monoid given?

I would not say that these monads are categorically dual, but rather that they are both produced by the following construction: given a monoidal category (C, ⊗, 1) and an algebra A in C, consider the monad sending X to A ⊗ X. In the first case, C is Hask, ⊗ is ×, and an algebra is a monoid, and in the second case C is Hask, ⊗ is ∐ (Either), and an algebra is just a type (every type is an algebra wrt ∐ in a unique way—this is what you refer to as a "comonoid", though that usually means something else, see below). As is customary, I am working in an imaginary world where ⊥ does not exist, so that × is actually a product and so on. It is probably possible to capture this common generalization using a suitable type class for monoidal categories (I am much too tired to understand what category-extras is trying to do in this regard at the moment) and thereby simultaneously define Writer and Either as monads (modulo newtypes, possibly).

As for the categorical dual of Writer m—well, it depends on what you want to consider as fixed, but the most likely candidate seems to be the comonad structure on (,) m without any conditions on m:

instance Comonad ((,) m) where
    coreturn (m, a) = a
    cojoin (m, a) = (m, (m, a))

(note that here is where we use that m is a comonoid, ie, we have maps m → (), m → m × m).

Here's the code:

{-# LANGUAGE FlexibleInstances, EmptyDataDecls, MultiParamTypeClasses,
FunctionalDependencies, GeneralizedNewtypeDeriving, UndecidableInstances #-}

import Control.Arrow (first, second, left, right)
import Data.Monoid

data Void
data Iso a b = Iso { from :: a -> b, to :: b -> a}

-- monoidal category (Hask, m, unit)
class MonoidalCategory m unit | m -> unit where
  iso1 :: Iso (m (m x y) z) (m x (m y z))
  iso2 :: Iso x (m x unit)
  iso3 :: Iso x (m unit x)

  map1 :: (a -> b) -> (m a c -> m b c)
  map2 :: (a -> b) -> (m c a -> m c b)

instance MonoidalCategory (,) () where
  iso1 = Iso (\((x,y),z) -> (x,(y,z))) (\(x,(y,z)) -> ((x,y),z))
  iso2 = Iso (\x -> (x,())) (\(x,()) -> x)
  iso3 = Iso (\x -> ((),x)) (\((),x) -> x)
  map1 = first
  map2 = second

instance MonoidalCategory Either Void where
  iso1 = Iso f g
         where f (Left (Left x)) = Left x
               f (Left (Right x)) = Right (Left x)
               f (Right x) = Right (Right x)

               g (Left x) = Left (Left x)
               g (Right (Left x)) = Left (Right x)
               g (Right (Right x)) = Right x
  iso2 = Iso Left (\(Left x) -> x)
  iso3 = Iso Right (\(Right x) -> x)
  map1 = left
  map2 = right

-- monoid in monoidal category (Hask, c, u)
class MonoidM m c u | m -> c u where
  mult :: c m m -> m
  unit :: u -> m

-- object of monoidal category (Hask, Either, Void)
newtype Eith a = Eith { getEith :: a } deriving (Show)

-- object of monoidal category (Hask, (,), ())
newtype Monoid m => Mult m = Mult { getMult :: m } deriving (Monoid, Show)

instance MonoidM (Eith a) Either Void where
  mult (Left x) = x
  mult (Right x) = x
  unit _ = undefined

instance Monoid m => MonoidM (Mult m) (,) () where
  mult = uncurry mappend
  unit = const mempty

instance (MonoidalCategory c u, MonoidM m c u) => Monad (c m) where
  return = map1 unit . from iso3
  x >>= f = (map1 mult . to iso1) (map2 f x)

Usage:

a = (Mult "hello", 5) >>= (\x -> (Mult " world", x+1))
                                 -- (Mult {getMult = "hello world"}, 6)
inv 0 = Left (Eith "error")
inv x = Right (1/x)
b = Right 5 >>= inv              -- Right 0.2
c = Right 0 >>= inv              -- Left (Eith {getEith="error"})
d = Left (Eith "a") >>= inv      -- Left (Eith {getEith="a"})

Strictly speaking, () and Void aren't dual--the presence of ⊥ means that all types are inhabited, thus ⊥ is the sole inhabitant of Void , making it a terminal object as you'd expect. () is inhabited by two values, so isn't relevant. If you handwave ⊥ away, then () is terminal and Void is initial as hoped.

I don't think your example is a comonoid structure, either--the signature for a comonoid should be something like this, I think:

class Comonoid a
    coempty :: a -> ()
    coappend :: a -> (a, a)

Which, if you consider what the equivalent comonoid laws must be, ends up being fairly useless, I think.

I wonder instead if what you're getting at is more closely related to the standard sum/product monoids over the naturals, as applied to algebraic data types? Void and Either are 0/+, while () and (,) are 1/*. But I'm not sure how to justify the rest of it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM