move rover 20 meters forward
Stateful Commands for the Mars Rover
Following up on the chapter "A mini DSL", I would like to include two new features in our DSL today:
-
command expressions like
move rover 20 meters forward
that read like plain english and -
a sequence of such commands for the same rover
Step 1: Command Expressions
Command expressions make code read like plain english, which is helpful (or just cool) in many DSLs. They require that we get rid of all punctuation and this can be really difficult in many languages. Luckily, it is a piece of cake in Frege. Let’s find out how we could implement the following example:
Now, this is just a simple move
function with four arguments! Done.
Well, almost done. We need to find out what type rover
should be and what the other
arguments are. Here is a proposal: just make rover
a Position with some nice record
syntax and use Double for all remaining arguments. The return value is the new position.
data Position = Pos { x, y :: Double }
derive Show Position
rover = Pos 0 0
meters = 1.0
forward = 1.0
move :: Position -> Double -> Double -> Double -> Position
move position distance unit direction =
position.{x <- (+ distance * unit * direction) } -- ➊
This should all be familiar, where at ➊ we modify the x position in a fashion that we already encountered in "The Power of the Dot".
Ok, this works nicely, but as soon as we try to do multiple moves in sequence, the code becomes rather unwieldy. We would have to pass the updated position around such that the result of the last move becomes an argument to the next move.
pos1 = move rover ...
pos2 = move pos1 ...
It would all be so much easier if we could just modify the position…
Step 2: Introducing State
Luckily, there is a State type in Frege and it perfectly fits our needs.
-
We can use a
State Position
type, meaning "state for a Position type". -
State
has amodify
operation that takes a high-order function, which knows how to update a position. Ha, this will be our newmove
! -
State
also has anexecState
that runs our stateful computations (doing the moves) starting from an initial position. Perfect.
When there is "bind" (in the form of the >>=
operator) then we can also use the do-nation
and we will do so in the final solution. Here it is.
module CommandExpressionRover where
import frege.control.monad.State
data Position = Pos { x, y :: Double }
derive Show Position
rover = Pos 0 0
feet = 0.305
meters = 1.0
forward = 1.0
back = -1.0
move ∷ Double -> Double -> Double -> State Position () -- ➊
move distance unit direction = State.modify update where
update pos = pos.{x <- (+ distance * unit * direction) }
with start f = State.execState f start -- ➋
main = do
endPosition = with rover do
move 20 meters forward -- ➌
move 10 feet back
println $ "moved the rover around, ending at " ++ show endPosition
This code prints
moved the rover around, ending at Pos 16.95 0.0
A few remarks about the code where it is not entirely obvious:
At ➊ we see in the type signature State Position ()
. Why the "()" unit? Well, in full
generality, State
has not only one type parameter but two:
State s a
, where s
is the type of the managed state - in our case Position
- and a
is the type that
stateful computations can evaluate to. We make no use of a
in our solution since we
are only interested in the state change. If you ever use
runState
or evalState
you will see the differences.
➋ introduced the small convenience function with
to flip the parameter positions
of execState
just because that makes the do-notation easier to use. I think it also
reads nicer. There is a flip
function to do this in-place but I found it distracting.
At ➌ we see that rover
has gone! It is now in the state. This is even more compelling as a DSL!
And this works 100% typesafe without implicit delegates (this is for the language geeks).
Stateless State
Many people claim that using state in a purely functional language is hard - and I concede that
it takes a bit more consideration than just writing position.x += distance * unit * direction
.
However, the code above is reasonably legible.
On the other hand: isn’t state undermining our purity goals? Have we silently converted to the dark side? No!
You may also have recognized that we got the plain endPosition
without the State
context as a result value.
This is only possible because State
is pure.
References
Haskell Wikibook |
https://en.wikibooks.org/wiki/Haskell/Understanding_monads/State |
Frege Language Reference |
http://www.frege-lang.org/doc/Language.pdf , section 3.2 "Primary Expression" |
Groovy Mars Rover DSL |