I am trying to find the most elegant way of converting the following stateful imperative piece of code to pure functional representation (preferably in Haskell to use abstraction that its Monad implementation offers). However I am not yet good at combining different monads using transformers and the like. It seems to me, that analyzing other's takes on such tasks helps the best when learning how to do it myself. The imperative code:
while (true) {
while (x = get()) { // Think of this as returning Maybe something
put1(x) // may exit and present some failure representation
}
put2() // may exit and present some success representation
}
When get
returns Nothing
we need the execution to continue with put2
, when get
returns Just x
we want the x
to get passed to put1
and short-circuit only if put1
fails or loop otherwise. Basically put1
and put2
may terminate the whole thing or move to the following statement changing the underlying state somehow. get
can either succeed and invoke put1
and loop or fail and continue to put2
.
My idea was something along:
forever $ do
forever (get >>= put1)
put2
And why I was looking for something like that is because (get >>= put1)
could simply short-circuit whenever get
has nothing to return or put1
terminates. Similarly put2
terminates the outer loop. However I am not sure how to mix the State
with the necessary Maybe
and/or Either
to achieve this.
I think using transformers to combine State
and the other monads is necessary and thus the code will most probably not be that succint. But I guess it as well might not be much worse.
Any suggestion how to achieve the translation elegantly is welcome. This differs from "Stateful loop with different types of breaks" in avoiding explicit control-flow using if
, when
, while
and rather tries to encourage use of Maybe
, Either
, or some other handy >>=
semantics. Also there is always a straight-forward way how to translate the code into a functional one, however it can hardly be considered elegant.
You are looking for
EitherT
orExceptT
. It adds two ways to return to a transformer stack. The computation can eitherreturn a
orthrowError e
. There are two differences between errors and returns. Errors are held on theLeft
and returns on theRight
. When you>>=
onto an error it short circuits.We will also use the names
left = throwError
andright = return
.Errors on the
Left
don't continue, we will use them to represent exiting from a loop. We will use the typeEitherT r m ()
to represent a loop that either stops with a breaking resultLeft r
or continues with aRight ()
. This is almost exactlyforever
, except we unwrap theEitherT
and get rid of theLeft
around the returned value.We'll come back to how to use these loops after fleshing out your example.
Since you want to see almost all of the logic disappear, we'll use
EitherT
for everything else too. The computation that gets data is eitherDone
or returns the data.The first computation that puts data is either a
Failure
or returns.The second computation that puts data is either a
Success
or returns.For your example, we will need to combine two or more computations that both stop exceptionally in different ways. We will represent this with two nested
EitherT
s.The outer
EitherT
is the one we are currently operating over. We can convert anEitherT o m a
to anEitherT o (EitherT i m) a
by adding an extraEitherT
layer around everym
†.The inner
EitherT
layer will be treated just like any other underlying monad in the transformer stack. We canlift
anEitherT i m a
to anEitherT o (EitherT i m) a
We can now build an overall computation that either succeeds or fails. Computations that would break the current loop are operated
over
. Computations that would break an outer loop arelift
ed.Overall
Failure
islift
ed twice into the innermost loop. This example is sufficiently interesting to see a few different results.†If
EitherT
had anMFunctor
instance,over
would just behoist lift
, which is a pattern that is used so often it deserves its own well thought out name. Incidentally, I useEitherT
overExceptT
primarily because it has a less loaded name. Whichever one provides anMFunctor
instance first will, for me, finally win out as the either monad transformer.You do not really need to combine different monads with combinators, you only need to explicitly embed the Maybe monad in the State monad. Once this is done, translating the snippet is straightforward, replacing loops by mutually recursive functions – the mutuality implements the branching conditions.
Let us write a solution this with OCaml and the sparkling monad library Lemonade where the State monad is called Lemonade_Success.
So, I assume that the type representing errors returned by put1 and put2 is a string, representing a diagnostic message, and we instantiate the Success monad on the String type:
Now, the Success module represents monadic computation which can fail with a diagnostic. See below for the complete signature of Success. I write the translation of the snippet above, as a functor parametrised by your data, but of course, you can shortcut this and directly uses the implementation definition. The data of your problem is described by a module Parameter having the signature P
A possible implementation of the snippet above would be
The function get_success maps the Maybe monad to the Success monad, providing an ad-hoc error description. This is because you need this ad-hoc error description that you will not be able to do this transformation using only abstract monad combinators – or, to phrase this, more pedantically, there is no canonical mapping from Maybe into State because these mappings are parametrised by an error description.
Once the success_get function is written, it is pretty straightforward to translate the branching conditions you described using mutually recursive functions and the Success.catch function, used to handle error conditions.
I leave you the implementation in Haskell as an exercise. :)
The complete signature of the Success module is
In order to stay succinct, I removed some type annotations and hid the natural transformation
T
from the signature.Your question is a bit tricky, because you are asking an elegant way of something which is not really elegant. There is the Control.Monad.Loops to write that type of loops. You'll probably need something like
whileJust'
or equivalent. Usually, we don't need to writewhile
loops like that and plain old recursion is usually easiest.I tried to find an example of when I would need this type of code and I came with the following example. I want to build a list of list of strings entered by the user. Each line correspond to an entry in the list. An empty line starts a new list, and two empty lines stops the loop.
Example
Will give
I would probably do the following in haskell
Just plain recursion.
This might overlap a bit with @Cirdec 's answer, but it also might help you gain a better perspective of what's going on.
The first thing to notice is that you really don't have doublely-nested loops. Without the exit statements, here is how you could write it as a simple loop:
So now we just use the standard technique of using
runEitherT
for breaking out of a loop.First some imports:
and our result type and a convenience function:
We then rewrite our loop lifting any IO actions and use
exit
when we want to break out of the loop:Here are some tests:
The output of these tests are:
In this example the returned value of
runEitherT
will always beLeft r
wherer
is theResult
value, so the code calling one of these examples might look like:Note that instead of a custom
Result
type you could just useEither String String
:and use
Left
forFail
andRight
forSuccess
.