title |
---|
Snake Revisited |
The previous chapter presented a program which implemented the game of
snake. That program used a monolitic step
function, that reacted to
each possible combination of input event and current state. The
program presented in this chapter —
SnakeRevisited.elm — is a revised version of
that program, in which the state-modifying function is composed from
several smaller functions.
The SnakeModel
, SnakeView
and SnakeSignal
modules are reused and
the SnakeState
and Snake
modules are replaced by new modules:
SnakeStateRevisited
and SnakeRevisited
. Additionally, a new auxiliary module
called Foldpm
is used as well.
Our goal is to replace the previously used monolithic step
by a set
of smaller functions that are composed together. The step
function
from the SnakeState
module had the following signature:
step : Event -> SnakeState -> SnakeState
Its implementation consisted of a case
expression, matching
combinations of the event and the gameOver
member of the current
state. Thus, there were several cases that were considered, but only
one of them was matched during a single function invocation. We want to
keep that semantics. We cannot thus simply split the individual
patterns of the case
expression into separate functions and compose
those functions using >>
or <<
, because that could cause code for
more than one case to be executed. Instead, our new step function will
have the following signature:
step : Event -> SnakeState -> Maybe SnakeState
We will decompose our old step
function into several smaller
functions with similar signatures, and the new step
function will be
a composition of those smaller functions. Let’s first examine the
individual smaller functions. Each of them corresponds to a pattern
from the old step
function.
The handleNewGame
function handles the NewGame
events. It returns
the initial state wrapped in Just
if that event is being processed,
and Nothing
otherwise.
% SnakeStateRevisited.elm
handleNewGame : Event -> SnakeState -> Maybe SnakeState
handleNewGame event _ = when (event == NewGame) initialState
The auxiliary function when
wraps its second argument in Just
if
the first argument is true, and returns Nothing
otherwise.
% Foldpm.elm
when : Bool -> a -> Maybe a
when p result = if p then Just result else Nothing
The handleGameOver
function returns the state unchanged (but wrapped
in Just
), if state.gameOver
is true. It returns Nothing
otherwise.
% SnakeStateRevisited.elm
handleGameOver : Event -> SnakeState -> Maybe SnakeState
handleGameOver _ state = when (state.gameOver) state
The handleDirection
function returns the state wrapped in Just
with
the delta
member potentially updated, when a Direction
event is
received. It returns Nothing
otherwise.
% SnakeStateRevisited.elm
handleDirection : Event -> SnakeState -> Maybe SnakeState
handleDirection event state =
case event of
Direction newDelta ->
Just { state | delta <- if abs newDelta.dx /= abs state.delta.dx
then newDelta
else state.delta }
_ -> Nothing
The handleTick
function handles the Tick
events, returning the
updated state wrapped in Just
if that event is being processed, and
Nothing
otherwise.
% SnakeStateRevisited.elm
handleTick : Event -> SnakeState -> Maybe SnakeState
handleTick event state =
case event of
Tick newFood ->
let state1 = if state.ticks % velocity == 0
then { state | gameOver <- collision state }
else state
in
if state1.gameOver
then Just state1
else let state2 = { state1
| snake <-
if state1.ticks % velocity == 0
then moveSnakeForward state1.snake state1.delta state1.food
else state1.snake
}
state3 = { state2
| food <-
case state2.food of
Just f ->
if state2.ticks % velocity == 0 &&
head state2.snake.front == f
then Nothing
else state2.food
Nothing ->
if isInSnake state2.snake newFood
then Nothing
else Just newFood
}
in
Just { state3 | ticks <- state3.ticks + 1 }
_ -> Nothing
The fact that the results of the above functions are wrapped in
Maybe
gives an additional piece of information. The result of
Nothing
means the function did not update the state and subsequent
functions (that the step
function is composed of) may potentially
try to update it. The result of Just
means that the function has
handled the state update and subsequent functions do not need to be
called.
We create the new step
function by composing the above
functions. However, since the result is wrapped in Maybe
, we cannot
use the regular function composition operators: >>
and <<
. Thus,
we compose the functions using an auxiliary function compose
:
% SnakeStateRevisited.elm
step : Event -> SnakeState -> Maybe SnakeState
step = Foldpm.compose [handleNewGame, handleGameOver, handleDirection, handleTick]
The compose
function is defined as follows:
% Foldpm.elm
compose : List (a -> b -> Maybe b) -> (a -> b -> Maybe b)
compose steps =
case steps of
[] -> \\_ _ -> Nothing
f::fs -> \\a b ->
case f a b of
Nothing -> (compose fs) a b
Just x -> Just x
It takes one argument, which is a list of functions. It returns a
function of the same type. The returned function is a composition of
the input functions. The composed function tries calling each of the
input functions one by one, until it finds one that returned a Just
result. That result becomes the final result of the composed
function. If none of the input functions returned Just
, the composed
function returns Nothing
.
There is one more issue that needs to be solved. The new signature of
step
does not conform to what the first argument of foldp
is
supposed to be. Thus, we cannot use foldp
directly. Instead, we
define the stateSignal
function using an auxiliary function
foldpm
.
% SnakeStateRevisited.elm
stateSignal : Signal SnakeState
stateSignal = foldpm step initialState eventSignal
The foldpm
function is defined as follows:
% Foldpm.elm
foldpm : (a -> b -> Maybe b) -> b -> Signal a -> Signal b
foldpm stepm b sa =
let step event state =
case stepm event state of
Nothing -> state
Just x -> x
in
foldp step b sa
It calls foldp
, passing it in the first argument the auxiliary
step
function, defined in the let expression. The step
function
calls the function passed to foldpm
as the first argument and
handles the result of that call. If the result is wrapped in Just
,
that result is simply unwrapped. If the result is Nothing
, the
step
function returns its second argument (the state) unchanged.
The handleNewGame
, handleGameOver
, handleDirection
,
handleTick
, step
and stateSignal
functions are defined in the
SnakeStateRevisited
module.
The revised game has its own main
function defined in the
SnakeRevisited
module:
% SnakeRevisited.elm module SnakeRevisited where
import Graphics.Element (Element)
import Signal ((<~), Signal)
import SnakeStateRevisited (..)
import SnakeView (..)
main : Signal Element
main = view <~ stateSignal
You can see that program in action here. From the user point of view it is analogous to the Snake.elm program presented in Chapter 13.
The foldpm
, when
and compose
functions are more general and not
specific to the snake program. They are defined in a separate module
called Foldpm
.