Skip to content

Instantly share code, notes, and snippets.

@Huxpro
Last active March 27, 2020 01:12
Show Gist options
  • Save Huxpro/f5f1fe91d70f28a0c0c294ec819e81a3 to your computer and use it in GitHub Desktop.
Save Huxpro/f5f1fe91d70f28a0c0c294ec819e81a3 to your computer and use it in GitHub Desktop.

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.

  1. we could use Existential type
data Writer a = forall w . Monoid w => Writer { runWriter :: (a, w) }  
  1. we could use GADT (with record syntax)
data Writer w a where
  Writer :: Monoid w => { runWriter :: (a, w) } -> Writer w a
  1. we could define a typeclass for MonadWriter, which is what mtl standard library did for Control.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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment