[Haskell-cafe] mtl: Why there is "Monoid w" constraint in the definition of class MonadWriter?

Andreas Abel andreas.abel at ifi.lmu.de
Mon Dec 10 18:21:18 CET 2012


I guess you have a point here:

1. The definition of the MonadWriter operations does not need the Monoid 
operations.

   Hence, the class constraint Monoid w should be removed.

2. The formulation of the MonadWriter laws (which are sadly missing from 
the documentation) would need the Monoid operations, though, e.g.

   tell v >> tell w  =  tell (v <> w)

   This is a weak indication that a constraint Monoid w should be 
present.  However, currently Haskell does not provide a formal 
specification of laws (except maybe as RULES!?), so having the 
constraint here is a bit too eager.  The story would be different in Agda...

Indeed, if one wants Roman's MyWriter to be an instance of MonadWriter, 
one needs to declare an annoying fake monoid instance for Integer.

{-# LANGUAGE MultiParamTypeClasses, DeriveFunctor, 
GeneralizedNewtypeDeriving #-}

import Control.Arrow
import Control.Applicative
import Control.Monad.Writer
import Data.Monoid

newtype MyWriter a = MyWriter { myWriter :: Writer (Sum Integer) a }
   deriving (Functor, Monad)

instance Monoid Integer where

instance MonadWriter Integer MyWriter where
   tell   w = MyWriter $ tell $ Sum w
   listen m = MyWriter $ (id *** getSum) <$> listen (myWriter m)
   pass   m = MyWriter $ pass $ (id *** \ f -> Sum . f . getSum) <$> 
myWriter m

Petr, you might wanna make a propsal to remove the Monoid w constraint 
to libraries at haskell.org, summarizing the current state of discussion so 
far.

Cheers,
Andreas

On 09.12.2012 11:04, Petr P wrote:
>    Hi all,
>
> I'd say that a type class declares functions and specifies laws (in the
> docs)
> what its implementations must adhere to. It's not the job of a type class to
> fulfill the laws, it's the job of its implementations. So the reason for
> 'Monoid w' in 'MonadWriter' cannot be that then 'MonadWriter' wouldn't be a
> monad. Such constraints should be required only by implementations.
>
> It is true that any Writer has an implicit underlying monoid, and we can
> even "extract" the operations from it as follows. The empty element can
> be extracted as
>
>       empty = liftM snd (listen (return ())) :: m w
>
> Having this 'empty', we can give 'const empty' to 'pass' to discard
> output of
> an action, so we can construct:
>
>      -- | @contained m@ executes the action @m@ in a contained
> environment and
>      -- returns its value and its output. The current output is not
> modified.
>      contained :: m a -> m (a, w)
>      contained k = do
>          -- we can retrieve mempty even if we don't have the monoid
> constraint:
>          ~(_, empty) <- listen (return ())

This seems a contrived way of getting 'empty'.  In this case, I prefer 
the monoid instance.

>          -- listen what @k@ does, get its result and ignore its output
> change:
>          pass (listen k >>= \x -> return (x, const empty))
>
> This generalizes 'listen' and 'pass' (both can be easily defined from
> it) and I find this function much easier to understand. In a way, it is
> also a generalization of WriterT's runWriterT, because for WriterT we have
> 'contained = lift . runWriterT'.
>
> [I implemented 'contained' in a fork of the mtl library, if anybody is
> interested: https://github.com/ppetr/mtl ]
>
> With that, we can do
>
>      -- Doesn't produce any output, only returns the combination
>      -- of the arguments.
>      append x y = liftM snd $ contained (tell x >> tell y) :: w -> w -> m w
>
> I didn't check the monoid laws, but it seems obvious that they follow
> from the
> monad laws and (a bit vague) specification of 'listen' and 'pass'.
>
> Personally, I'd find it better if `MonadWriter` would be split into two
> levels:
> One with just 'tell' and 'writer' and the next level extending it with
> 'listen'/'pass'/'contained'. The first level would allow things like
> logging to
> a file, without any monoidal structure. But this would break a lot of stuff
> (until we agree on and develop something like
> http://hackage.haskell.org/trac/ghc/wiki/DefaultSuperclassInstances).
>
>      Best regards,
>      Petr
>
>
>
> 2012/12/9 Roman Cheplyaka <roma at ro-che.info <mailto:roma at ro-che.info>>
>
>     * Edward Z. Yang <ezyang at MIT.EDU <mailto:ezyang at MIT.EDU>>
>     [2012-12-08 15:45:54-0800]
>      > > Second, even *if* the above holds (two tells are equivalent to one
>      > > tell), then there is *some* function f such that
>      > >
>      > >     tell w1 >> tell w2 == tell (f w1 w2)
>      > >
>      > > It isn't necessary that f coincides with mappend, or even that
>     the type
>      > > w is declared as a Monoid at all. The only thing we can tell
>     from the
>      > > Monad laws is that that function f should be associative.
>      >
>      > Well, the function is associative: that's half of the way there to
>      > a monoid; all you need is the identity!  But we have those too:
>      > whatever the value of the execWriter (return ()) is...
>
>     Let me repeat:
>
>        It isn't necessary that f coincides with mappend, or even that the
>        type w is declared as a Monoid at all.
>
>     Let me illustrate this with an example.
>
>        data MyWriter a = MyWriter Integer a
>
>        instance Monad MyWriter where
>          return = MyWriter 0
>          MyWriter n x >>= k =
>            let MyWriter n' y = k x
>            in MyWriter (n+n') y
>
>        instance MonadWriter Integer MyWriter where
>          tell n = MyWriter n ()
>          listen (MyWriter n x) = return (x,n)
>          pass (MyWriter n (a,f)) = MyWriter (f n) a
>
>     Yes, integers do form a monoid when equipped with 0 and (+). However, we
>     know well why they are not an instance of Monoid — namely, there's more
>     than one way they form a monoid.
>
>     Even if something is in theory a monoid, there may be technical reasons
>     not to declare it a Monoid. Likewise, imposing a (technical) superclass
>     constraint on MonadWriter has nothing to do with whether the Monad will
>     be well-behaved.
>
>     This is true in both directions: even if the type is an instance of
>     Monoid, nothing forces the Monad instance to use the Monoid instance.
>     I.e. I can declare a MonadWriter on the Sum newtype whose bind, instead
>     of adding, subtracts the numbers.
>
>     Roman

-- 
Andreas Abel  <><      Du bist der geliebte Mensch.

Theoretical Computer Science, University of Munich
Oettingenstr. 67, D-80538 Munich, GERMANY

andreas.abel at ifi.lmu.de
http://www2.tcs.ifi.lmu.de/~abel/



More information about the Haskell-Cafe mailing list