This time I've solved it using a finite state machine implemented with mutually recursive functions and trampoline.
Mutually recursive functions are a nice functional way to implement finite state machines and it's a functional way of expressing the state pattern (see Functional Programming Patterns in Scala and Clojure, Michael Bevilacqua-Linn).
As in the previous example using protocols, we have four possible states of the rover:
- The rover is facing north
- The rover is facing east
- The rover is facing south
- The rover is facing west
- Rotate left
- Rotate right
- Move forwards
- Move backwards
These states and transitions can be directly translated to a set of mutually recursive functions by associating each state to a function:
- The rover is facing north -> facing-north
- The rover is facing east -> facing-east
- The rover is facing south -> facing-south
- The rover is facing west -> facing-west
This is the new code of the rover name space:
Notice how inside each of the functions representing rover states (facing-north, facing-east, facing-south and facing-west), there is a case on the command which determines which of the functions will be called next, i. e., which is the next rover state.
Once the next function is selected, we pass it the rest of the commands (notice how destructuring is used to succinctly extract the first command and the rest of commands).
The recursion ends when there are no more commands (command will evaluate to nil), in which case we return the current-rover.
Ok, now some technical details:
-
Trampoline function. The trampoline function is used in order to make Clojure optimize the mutual recursion.
Each state function returns a function returning a value (notice the # tacked onto the front of the outer level of each state function) instead of directly returning the value. This is required by the trampoline function so it can manage the stack on the mutually recursive calls to avoid stack consumption for long command sequences.
To start the trampoline we feed it with the initial state function (given by get-initial-state-fn) and its parameters. - letfn form. Using letfn allows to create local functions which can refer to each other. This can't be done using let because it executes its bindings serially.
The commands name space now just translates from the received character signals to keywords representing each command:
The worlds name space is nearly the same, I just added two helpers (wrap and rover-not-hitting-obstacle) so that the functions that do the wrapping and the collisions with obstacles checking are easier to use in rover name space (they also help to hide the inner structure of worlds):
Finally, the core name space is much simpler now:
I'm using exactly the same tests I used in the three previous solutions:
- Kata: Mars Rover in Clojure using multimethods
- Separating Mars Rover code into different name spaces
- Mars Rover code version using protocols instead of multimethods
You can find the code of this last example in this GitHub repository.
I've discovered this other way of doing this in these two great books:
- The Joy of Clojure, Second Edition by Michael Fogus and Chris Houser which I'm enjoying reading at the moment and from which I've got many of the explanations in this post. I'm learning a lot with it.
- Functional Programming Patterns in Scala and Clojure by Michael Bevilacqua-Linn which is a great book from which you can learn how to express many design pattens in a functional (and most of the times much simpler) way.
No comments:
Post a Comment