The following Haskell code is a simple "console IO" DSL:
data Ap a = Ap { runAp :: ApStep a }
data ApStep a =
ApRead (String -> Ap a)
| ApReturn a
| ApWrite String (Ap a)
ioRead k = Ap $ ApRead k
ioReturn a = Ap $ ApReturn a
ioWrite s k = Ap $ ApWrite s k
ioWriteLn s = ioWrite (s ++ "\n")
apTest =
ioWriteLn "Hello world!" $
ioRead $ \i ->
ioWriteLn ("You wrote [" ++ i ++ "]") $
ioReturn 10
uiRun ap =
case runAp ap of
ApRead k -> uiRun (k "Some input")
ApReturn a -> return a
ApWrite s k -> putStr s >> uiRun k
run = uiRun apTest
It works OK however I would like to write the "application" apTest using a monad instead of using $. In other words like this:
apTest = do
ioWriteLn "Hello world!"
i <- ioRead
ioWriteLn $ "You wrote [" ++ i ++ "]"
return 10
The problem is that the code is resisting all my attempts to turn the "function style" DSL into a monad. So the question is how to implement a monad instance for this DSL that allows you to write apTest monad style instead of "$" style?
I think this is what you were aiming for. The only change I made was to condense
Ap
andApStep
into a single type.Although written in monadic style using
do
notation,apTest
is identical to the following hand-written chain of constructors:This is a special case of a free monad, so you can simplify your code dramatically by just writing:
To learn more about free monads, you can read my post on free monads, which goes into more detail about how you convert DSLs to free monads and builds an intuition for how they work.
Sure it's a monad. In fact it would be simpler to express it as a free monad [1] but we can work with what you've got.
Here's how we know it's a monad: if you have a type
data Foo a = ...
whereFoo
represents some sort of recursive tree structure wherea
s only occur at the leaves then you have a monad.return a
is "give me a tree consisting of one leaf labelled bya
", and>>=
is "substitution at the leaves".In your case
Ap
is a tree structure whereApReturn a
is a leafthere are two sorts of interior nodes
ApRead
is a node which has no label and has one descendant coming off it for every value of typeString
ApWrite
is a node which is labelled by aString
and has only one descendant coming off itI've added the monad instance to your code below.
return
is justAppReturn
(plus theAp
wrapper).>>=
is just recursively applying>>=
and substituting at the leaves.A couple of hints for the future
Ap
wrapper was unnecessary. Consider removing it.Enjoy!
[1] http://www.haskellforall.com/2012/06/you-could-have-invented-free-monads.html
Is my thing a monad?
I don't have any specific help for you, but I have a bit of general guidance that is way too long for a comment. When my intuition tells me I want to make something an instance of
Monad
, the first thing I do is sit down with a pen and a piece of paper and I ask myself,As it turns out, a lot of times it isn't – it was just my intuition wanting to jump on the bandwagon a bit too quickly. You can't very well create a
Monad
instance for your thing if your thing isn't a monad. Here's my checklist of three things that need to be covered before I call my thing a monad.When I have decided that my thing is a monad, I have usually also in the process accidentally come up with everything I need to create a monad instance for my thing, so this is not a useless exercise in rigor. This actually will give you the implementations of the two operations you need to create a monad instance for your thing.
What are monads?
For your thing to be a monad, it needs to have two operations. These are commonly, in the Haskell world, referred to as
return
and(>>=)
(pronounced "bind".) A monad can be seen as a computation with a "context" of some kind. In the case ofIO
, the context is side effects. In the case ofMaybe
, the context is failure to provide a value, and so on. So a monad is something that has a value, but there's something more than just the value. This something more is often referred to as a "context" for lack of a better word.Operations
Anyway, the signatures involved are
This means that
return
takes any old valuea
and puts it into the context of your monad somehow. This is often a fairly easy function to implement (there aren't too many ways you can put anya
value into a context.)What's interesting is
(>>=)
. It takes a valuea
inside your monad context and a function from any valuea
to a new valueb
but inside your monad context. It then returns theb
value with context. You need to have a sensible implementation of this before you even consider making aMonad
instance for your thing. Without(>>=)
, your thing is definitely not a monad.Laws
However, it isn't enough to have
return
and(>>=)
! I said the implementation needs to be sensible. This also means that your thing must have implementations ofreturn
and(>>=)
that obey the monad laws. They are as follows:return a >>= f
should be the same thing asf a
m >>= return
should be the same thing as justm
(m >>= f) >>= g
should be the same thing asm >>= (\x -> f x >>= g)
These make a lot of sense* (the first two are trivial and the third one is just an associativity law), and we need all monads to obey them. This is not checked by the compiler (but it might assume they will hold) so it is your responsibility to make sure they hold.
If your monad-to-be obeys these laws, you get a monad! Congratulations! The rest is just paperwork, i.e. defining the instance as
and then you are ready to use
do
syntax as well!* More information on the Haskell wiki page on monad laws.