OK, so today I was looking at Writer
monad, a very basic Monad.
In its essence, it's just a Monad that outputs (e.g. log) something,
The "Variation three: Output" Monad from the original Monads for functional programming, 92 paper is very lean:
type M a = (Output, a)
type Output = String
Which is essentially just a parametric tuple:
type Writer a = (a, String)
However, because of the current Haskell's limitation of "normal type cannot be made into instances of typeclasses",
It has to be wrapped within a dummy data constructer. We will use the restricted version of data
, newtype
to emphasize the correspondence between the new type Writer
and the constructor Writer
:
newtype Writer a = Writer (a, String)
The consequence is that we will have to unwrap from the constructor when using it...
unwrap (Writer m) = m
People tend to name this datacon differently for different monads, e.g. parse
for Parser.
It's not so good as we sometimes want to name them consistently and even constraint the name from where the monads are defined.
Record syntax provide a nice way to do it, which is essentially a syntax sugar of tuple plus it will generate field accessing function.
data Writer a = Writer { runWriter :: (a, String) }
Now Writer
is the wrapper (or unit from the algebraic sense) and the runWriter
is the reserve operation of it.
So far so good. But the String
here is too specific. Let's generalize it as another type variable w
as the parateriz-able "writes" type.
data Writer w a = Writer { runWriter :: (a, w) } -- orderred for the ease of partial application
Well, did you see any potential problems? Not every type could be appended in the manner of outputs.
Monoid
to the rescue! let's add a type class constraint to our data constructor
data Monoid w => Writer w a = Writer { runWriter :: (a, w) }
Now this looks really nice.
However, this feature has been disallowed after GHC 7.2. I have no idea why though it could be turned back on with -XDatatypeContexts
.
So what do we do if we do not want this ceremony?
Declaimer: I haven't fully understood the below things and that's why I complained.
- we could use Existential type
data Writer a = forall w . Monoid w => Writer { runWriter :: (a, w) }
- we could use GADT (with record syntax)
data Writer w a where
Writer :: Monoid w => { runWriter :: (a, w) } -> Writer w a
- we could define a typeclass for
MonadWriter
, which is whatmtl
standard library did forControl.Monad.Writer.Class
class (Monoid w, Monad m) => MonadWriter w m | m -> w where
(| m -> w
is https://wiki.haskell.org/Functional_dependencies and could be ommited)
The story almost ends except the Monad Transformer.
the Writer
monad in the current standard library is defined from WriterT
:
https://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Writer-Lazy.html#t:Writer
so we have WriterT
and the monad itself becomes type parameter m
newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }
type Writer w = WriterT w Identity
runWriter :: Writer w a -> (a, w)
runWriter = runIdentity . runWriterT
🙃WTF are there so many ceremonies?