In this post I will look at the ideas behind programming user interfaces and present a method for building them in a pure functional setting.
- In Part 1 I explain the fundamental ideas behind building interactive programs.
- In Part 2 I look at how React/Flux implements these ideas.
- In Part 3 I describe a set of combinators for building a pure update function.
- In Part 4 I present an implementation of these combinators in Haskell, as well as some example code.
Part 1: What’s Going on in the Front End
Let’s start by dividing all computer programs into two buckets. We’re talking about desktop apps, phone apps, websites, command line utilities, everything.
In the first bucket we’ll put non-interactive programs. These programs take all their input up front, and then do something or give back some answer. Most command line programs act in that way.
$ ls
Foo.txt Bar/
Non-interactive programs are easy to write.
I say ‘easy’ because their structure is simple, although of course I am being a bit tongue in cheek. Compilers and media transcoders would fall into this category, and it wouldn’t be easy to write their algorithms or make them fast, but their structure is always the same. It’s a pipeline that takes the initial inputs and transforms them in some way, yielding some outputs. And these programs will be built this way until the end of time! There’s really no other way to do it.
(Take in Input) ---> ... ---> (Produce Output)
The second bucket, of course, holds interactive programs. And really, most programs that people use on a daily basis are interactive.
Interactive programs are much harder to structure. Or maybe instead I should say that the piece of the program that handles user interaction is hard to structure. After all, it earns its own name: the front end.
There are so many ideas out there for how to build them (object oriented versions of MVC, FRP, “The Elm Architecture”, and React/Flux, among many others), but nothing approaches concensus. And the consequences for building a front end badly include: bugs (including very subtle ones), slow development time, and inflexible code that cannot adapt to changing requirements. So it’s a pretty interesting problem to work on!
What Is an Interactive Program?
Interactive programs run in a loop. On each iteration, they take in some input (like a mouse click or key press), and use it to update their state (also called the Model), and then perhaps show some representation of the state to the user (also called rendering or viewing).
<Loop> ---> (Take in Input) ---> (Update State) ---> (View) ---> <Back to Loop>
The view part is structurally pretty uninteresting. It takes the Model and generates some view representation– maybe HTML or OpenGL commands. It’s straightforward in the same way that pipeline programs are.
But the update loop! That’s where all the conflict arises. And the need to perform these updates elegantly is what drives the invention of so many paradigms and frameworks. Most of them boil down to a simple state machine. In other words, given some input, we need to find a new state for our program. There are many ways to model this, but I think one way worth exploring is the simplest: the function.
So at the core of the update loop could be a single function (the update function), which takes in input and produces a new program state.
What Is an Update Function?
An update function takes as arguments the previous state of the program as well as some input value, and it returns a new input state.
Old State + Input = New State
If you’re familiar with MVC, then it’s roughly the “Controller” part of MVC, but expressed as a pure function. I’ll call this function ‘update’. A C-like language might use it like so:
Model appState = newAppState(); // Create the initial program state
while(true) {
foreach (Input input in getInputs()) {
appState = update(appState, input); // Update the program state for each new input
}
renderScreen(appState); // Update the view based on the new program state
}
So again, we’re going to ignore all of that code except for the update function, which is really the interesting part. Now, what might be inside of such a function? You can imagine an update function for a game might be something like, “if Mario is standing on solid ground, and the player hit the Jump button, then send Mario into the air.” And furthermore, since our update function is pure, that means it can’t directly modify the old state, but instead must produce a new state. So really it would say, “Given a state where Mario is standing on solid ground, and an input where the player hit the jump button, give back a state where Mario is heading up into the air.”
The type signature for update is:
Model update(Model, Input);
Let’s move over to Haskell syntax, as it’s a bit more natural language for talking about types. In Haskell we have:
update :: Model -> Input -> Model
That means ‘update’ has a type which is a function that takes a Model and an Input and yields a Model. (The return type is just the last type in the arrow list.)
What Makes Interactive Programs So Dang Complicated
The real difficulty is that, in some sense, the update function will change its behavior depending on the current state, which makes it more challenging to design well.
Here’s an example:
Let’s say you’re writing a drawing program. It has a main drawing area and some different tools you can select on the left, and some typical menu buttons up top. If you click on the pencil tool, that tool is selected. And then clicking on the canvas will draw with the pencil. Clicking on the square tool will select it, and then clicking on the canvas will create squares. So, clicking on the canvas will have a different meaning depending on what the selected tool is. And clicking on the tools has a different meaning (tool selection) than clicking on the canvas (drawing).
------------------------
|[New] [Open] |
------------------------
|[Pencil]| |
|[Square]| /\ /\ |
|--------| |
| ^ |
| |
| \_____/ |
| |
------------------------
So it’s not just a static pipline from input to output. The program needs to keep track of some state (for example the currently selected brush), and the meaning of the input changes based on the current state. In a way, the pipeline itself changes.
Actually, I don’t think I should call it a pipeline at all, because in an interactive program the inputs aren’t carried directly from start to finish down one path. Instead, the input flow branches. Like if you clicked “New” and a dialogue box popped up:
----------------------------------------------
| Are you sure you want to erase everything? |
| |
| [Yes] [Cancel] |
----------------------------------------------
If you click ‘Yes’, the program needs to both close the dialogue box and also erase the canvas, which are really two very unrelated things. I can draw the input flow like this:
---> (Update Dialogue Boxes) --
<Loop> --> (Take in Input) -| |--> (View) --> <Back to Loop>
---> (Update Canvas) ----------
So the input needs to go to multiple places (carrying different meanings!).
Code That Does Multiple Unrelated Things
This is really the crux of the matter. Closing dialogue boxes is unrelated to drawing pictures, so the dialogue box state should be kept separate from the drawing state, and the code that runs these systems should be kept separate as well. There are huge benefits for doing this properly.
If our application draws pictures, then we should be able to reuse some kernel of it in many different contexts– web app, phone app, command line, or even an automatic picture drawing AI. Likewise, the UI code should understand dialogue box behavior but NOT anything about drawing pictures. That will allow us to use the same UI code in many different applications. So by keeping these pieces separated, we get code that is more reusable, which in turn makes it more readable, less buggy, and likely better tested as well. It’s really the road to success.
But how can we separate these things, when they are clearly tied so closely together at some point in our application? It helps to think in terms of input types.
From the perspective of our drawing code, the inputs are commands like “Draw Pixel at coordinate X/Y” or “Draw Square from X/Y to W/H” or “Erase Drawing”, and it doesn’t matter what the actual raw input is. (I.e., our drawing code shouldn’t care whether it takes in mouse clicks or button presses or anything else.) On the other hand, our UI takes inputs like “Yes Button Clicked”, and it should be able to close the dialogue without understanding what the “Yes” button is really supposed to do.
To Summarize
We want to build our application out of multiple unrelated pieces (i.e. multiple update functions), which we then glue together to create the final update function, that we plug into the update loop to drive our interactive application.
(Instead of the long-winded name “update function”, I’m going to start calling them “layers”. The name makes sense if you think of building the final update function by stacking layers of different functionality.)
This glue code will be able to translate our raw inputs into the specific input types needed by each layer.
AND, as we saw in the drawing example, sometimes the translation of the inputs will be affected by the state of some layer. We can’t always turn a click on the canvas into a Draw Pixel Input. Sometimes it’s a Draw Square Input, depending on the state of the UI.
AND, as we saw in the dialogue example, sometimes one input will need to be carried to multiple places.
So, the interesting part of an interactive application is the update function. And the interesting part of an update function is the glue code that binds the disparate pieces of the application together. So that glue code is what we will focus on from here on out!
Part 2: How Does React/Flux Implement These Ideas?
Facebook has a project called React used for building interactive websites, and a Flux architecture for managing application state. Together, they handle all the stuff I just wrote about, although not generically. You might not care about React/Flux, but bear with me. I think it’s interesting to look at how this problem is tackled commerically, and where there might be room for improvement.
So the key point here is that React layer maintains the UI state and translates raw input (usually HTML link clicks) into Flux actions (i.e. the application level inputs), and then Flux is basically just a simple update function that translates those actions into new application state.
It’s a limited version of what I described above. There are two states– application state (in the Flux store) and UI state (in the React components). And the raw input, link clicks, will both update the UI state and also be translated into Flux actions which are then used to update the Flux store.
So React/Flux has two layers, and just to be clear, here’s a table describing them by their state and input types:
Layer 1: State Type = UI State, Input Type = Browser Clicks
Layer 2: State Type = Application State, Input Type = Flux Actions
Now, don’t get me wrong, this works out pretty well! React/Flux has many happy customers, and it’s growing in popularity.
But you can only have two types of state (application state and UI state). And you can only have two input types, the raw mouse clicks you get from the browser, and the Flux actions. This works fine for many applications, but that doesn’t mean it is ideal.
What if we could have any number of states, any number of input types, any number of layers, and any sort of relationship between the layers?
Wait, Why? Is This Really a Problem?
In a simple example, it’s pretty clear where to draw the line between the two layers. But as an application grows, there may be more and more opportunities to split it into multiple layers. Instead of just one UI layer at the top, it may make for better architecture to have many.
Going back to our drawing example, our canvas might have some overlay that is affected by tool selection, but is still considered an interface to the core drawing functionality. Perhaps there are some draggable nodes that show up on the canvas when the line tool is selected. Now imagine those node handles have their own state (like if you click on one it changes the dragging style from snapping to smooth), React/Flux would force you to make a binary choice about whether that new state is considered UI or Application, and whether the inputs that the node dragging generates are considered “Flux Actions”. We are clearly talking about some form of UI, but is this really the same level as the overall UI? Or is it more closely tied to the drawing paradigm?
Sometimes it’s not clear whether something is “application” or “UI”. Sometimes it doesn’t fit squarely in either camp. Sometimes you need an intermediate layer.
Luckily, it’s no extra work to be more general here. The 2 layer React/Flux design is just a special case of a more general solution that I’ll outline in the next section.
Part 3: Combinators for Building Pure Update Functions
Just to unpack that title a bit:
- Pure Function - A function that returns a value calculated only from its arguments. It cannot reference or modify state outside the function.
- Combinator - A function that takes functions as arguments and returns a new function.
So, what do I mean by ‘building’ an update function? Well it would be insane to write out an entire application as one giant function. As mentioned up above, good structure comes from separating unrelated modules of code. So we’ll assume that we can write smaller update functions to handle each part of the application, and then we can use some combinators to bring them all together into one master update function.
This is the ‘glue code’ I talked about earlier. We need a library of functions that will let us glue layers together!
This section will introduce the combinators with a little bit of Haskell syntax, but will be light on details. I want to give the flavor of building UIs in this way, without getting lost in the particulars. The following section will provide actual implementations and examples.
Side-note for Experienced Functional Programmers
Modeling these update functions as having the type (state -> input -> state) affects the ways we can combine them. Instead, we could model them using the reader and state monads, which would mean we could use more of the standard haskell library to manipulate them. However, in practice I’ve found that usually it reads better to just write the simple functions. But another approach would be to provide some helpers to convert between the two models:
type Layer s i = ReaderT i (State s) ()
unlayer :: Layer s i -> s -> i -> s
unlayer a s i = execState (runReader a i) s
layer :: (s -> i -> s) -> Layer s i
layer f = do
s <- get
i <- ask
put (f s i)
Crash Course in Haskell Syntax
If you can read C, then this will get you pretty far. The following two code blocks are roughly equivalent:
// C
int foo(int bar, int baz) {
return bar + baz;
}
foo(3,4);
___________________________
-- Haskell
foo :: Int -> Int -> Int
foo bar baz = bar + baz
foo 3 4
So, in Haskell, function names and arguments always start with a lowercase letter, while type names start with a capital. And the type signature of a function is written on a separate line from the implementation.
Now, onto the combinators!
Simple Composition
Imagine our program is a game and its state is made up of two smaller states– the UI state and the Game state. In Haskell we could write that like this:
-- The Model type is a pair consisting of two other types, UIState and GameState
type Model = (UIState, GameState)
If we have an update function for each piece of state, then we need a function to combine them so that a single input is passed into both of them. It’s so fundamental that we can make up an operator for it, which I’ll write as *.*.
Using this operator, here’s a possible definition of our update function.
update = updateUI *.* updateGame
That, again, is Haskell syntax, and it can be read as, “The update function is equal to the updateUI function combined with the updateGame function using the *.* operator.”
Note that this function will give you the updated state, but what to do with it is up to you! In other words, everything we build here is just one big pure function. (Presumably, you plug it into some update loop.)
Each smaller update function has the same type as the big one, which makes it easy to combine them. We can write their type signatures like this:
update :: Model -> Input -> Model
updateUI :: Model -> Input -> Model
updateGame :: Model -> Input -> Model
Although the updateGame function operates on only a portion of the Model state, it’s type signature demands that it take in the entire Model. Let’s see if we can fix that.
Scoping Updates
It would be nice to define our updateGame function in a way that completely isolates it from the UI state. In other words, we want the type of updateGame to be GameState -> Input -> GameState.
Luckily, the functional community has already figured out a nice technique for doing this. Using something called a lens, we can apply a function to just one part of a whole. The only lenses I’ll use here are the ones that pick out individual pieces of a tuple. So for our pair, we have **_1, which applies to the first item in a pair, and _2**, which applies to the second item.
Now we just need a combinator that uses a lens to scope an update function to a particular part of the Model. I’ll call it liftState.
update = (liftState _1 updateUI) *.* (liftState _2 updateGame)
I added parentheses in the update definition to make it easier to read, but the basic idea is that the first argument to liftState is a lens to use and the second is the update function we want to apply.
Pay attention to how the types of the layers changed. Now the updateGame function doesn’t have any knowledge of the UIState type, and similarly updateUI is isolated from the game state. Here are the new type signatures.
update :: Model -> Input -> Model
updateUI :: UIState -> Input -> UIState
updateGame :: GameState -> Input -> GameState
If you want to learn more about lenses, check out: http://www.haskellforall.com/2013/05/program-imperatively-using-haskell.html
Changing Inputs
Up until now, we haven’t looked inside of the Input type. For a real program it could have a definition like this:
data Input = KeyDown Key
| KeyUp Key
| Click Point
| Tick Double
That means an Input could either be a KeyDown, or a Click, etc… You can think of it as an enum, but where each member can carry additional data.
The last one, Tick, can be used to step forward the simulation or the UI. Its “Double” member would be the time delta since the last update. One of these inputs would be emitted every frame.
So just as we changed the state basis of our sub layers, we might also want to change what input they receive. For example, our game layer should take in messages like “PlayerJump” instead of “KeyDown SpaceBar”. This will make our code more reusable. It will be easy to have, say, a touchscreen interface as well as a keyboard interface without changing our game logic at all.
We’ll accomplish this with a combinator called “liftInput”. Its first argument is a function that transforms inputs in some way, and its second argument is an update function. So our new master update function might look like this:
update = liftState _1 updateUI *.* liftState _2 (liftInput playerControls updateGame)
playerControls should just be a function that transforms raw inputs to game inputs. So it could look like this:
data GameInput = PlayerJump
| PlayerMoveRight
playerControls (KeyDown SpaceBar) = PlayerJump
playerControls (KeyDown Right) = PlayerMoveRight
...
In Haskell, you can give multiple definitions for a function where each matches a different pattern for the input. So depending on what key is pressed a different definition of playerControls will be used.
I should note the new type of updateGame.
updateGame :: GameState -> GameInput -> GameState
See how nice and domain specific it is?
Conditionals
Now let’s say we want our game to have a pause menu. You can imagine added complexity here, since upon pausing we want to completely disable the game layer. But we can add another combinator to make this easy.
when will take two functions as arguments. The second, as usual, is an update function that may or may not be run. The first is a predicate on Model. In other words, it’s a function that determines whether or not we are paused.
So if we have a paused function:
paused :: Model -> Bool
Then we can write our update function like this:
-- Split into multiple lines for readability
update = liftState _1 updateUI *.*
when (not . paused) (liftState _2 (liftInput playerControls updateGame))
To make it easier to read we can pull these pieces out into separate functions. So this is equivalent to the above:
uiLayer :: Model -> Input -> Model
uiLayer = liftState _1 updateUI
gameLayer :: Model -> Input -> Model
gameLayer = liftState _2 (liftInput playerControls updateGame)
update = uiLayer *.* when (not . paused) gameLayer
Post-Processing
Sometimes we want to transform the output of an update function. As an example, we might want to add something called a ‘time-traveling debugger’. The basic idea is that we record the new states that are generated by an update function, and later we can browse through them to see where a problem occured. (For a nice example of this technique in action, check out this video demonstrating the JS framework Redux’s implementation of a time traveling debugger.)
The implementation details require a little more Haskell knowledge, but once they are ready to go it’s really easy to add to a project.
First we’ll change our Model type a bit so that the GameState is wrapped inside of a type called Debug. This basically means that we’ll be storing multiple GameStates instead of just one.
type Model = (UIState, Debug GameState)
Now we just postProcess our game layer with a function called debugResolver, which will handle storing new game states as they are produced, and picking out a specific game state as we step back in time.
update = uiLayer *.* postProcess debugResolver gameLayer
Interactive UI
Until now, we haven’t actually seen an example equivalent in power to React. In other words, we haven’t seen this:
- Take in raw input (clicks)
- Update UI (e.g., close dialogue box).
- Generate app-specific messages (e.g., erase drawing)
- Update app state with any generated messages
But we do have nearly everything we need to do it. It’s really just a combination of liftInputs and liftStates, plus one extra thing: a way to change the liftInput behavior depending on the UI state. Afterall, we only want to generate an “erase” message when the proper button is clicked, and sometimes that button isn’t even on the screen.
That ‘one extra thing’ is the dynamic combinator. It simply passes in the current state so that it can be used in building the update function. With it we can build a function, mainLayer, which takes a Model and then returns an update function.
-- this function, 'mainLayer', has an argument which is the current state
-- it returns an update function
mainLayer :: Model -> (Model -> Input -> Model)
mainLayer currentState = liftInput (uiClick currentState) subLayer
update :: Model -> Input -> Model
update = dynamic mainLayer
-- we'll use a type called 'UIClick' to describe some representation
-- of the UI action. I.e., maybe it describes a particular menu item
-- being picked, or a particular dialogue box button being clicked
type UIClick = ...
-- this function, 'uiClick', takes in the current state and returns
-- a function that translates raw inputs to a UIClick
uiClick :: Model -> (Input -> UIClick)
uiClick = ...
-- this update function operates on 'UIClicks' instead
-- of raw inputs
subLayer :: Model -> UIClick -> Model
subLayer = liftState _1 updateUI *.* liftState _2 (liftInput gameClick updateGame)
-- I won't provide implementations for these functions,
-- but you can tell from the type signatures what they might do
-- 'gameClick' will translate 'UIClicks' into the normal 'GameInputs'
-- that the updateGame layer understands
gameClick :: UIClick -> GameInput
gameClick = ...
updateUI :: UIState -> UIClick -> UIState
updateUI = ...
updateGame :: GameState -> GameInput -> GameState
updateGame = ...
That’s the whole structure of React/Flux in a few lines of code, and it’s more powerful to boot! If it’s just one more tool in your toolbox instead of some all-powerful “architecture”, then surely your codebase will grow more flexible over time.
Part 4: Implementations and Examples
This section will cover the same ground as Part 3, but in more detail.
Just a warning– all the code will be in Haskell from here on out. I’ll give some more examples of Haskell syntax that I’ll use, but if you want a more expansive introduction, check out this guide: http://prajitr.github.io/quick-haskell-syntax/
Haskell’s sum-types can be made this way:
-- Haskell
data Day = Monday | Tuesday
This is roughly equivalent to:
// C
enum DAY { monday, tuesday };
A record type looks like this:
-- Haskell
data Point = {
x :: Int,
y :: Int
}
This is roughly equivalent to:
// C
struct Point {
int x;
int y;
};
Haskell types can take arguments. Here are some syntax examples. -
type Foo a = (a,a) -- this means "Foo Int" would be a synonym
-- for the pair type "(Int, Int)"
type Bar b = String -> b -- this means "Bar Int" would be a synonym
-- for the function type "String -> Int"
Code
As in part 3, let’s say we’re building a game, and we have a Model that has both UIState and GameState.
-- UIState and GameState can be any types. We won't need to look
-- inside them for the example.
type UIState = ...
type GameState = ...
-- Our Model type will be a composite of the two:
data Model = {
_uiState :: UIState,
_gameState :: GameState
}
And the raw input we get is either key presses or clicks or frame ticks.
data Input = KeyDown Key
| KeyUp Key
| Click Point
| Tick Double
So ultimately we’re going to have an update function of type Model -> Input -> Model. In order to talk about these update functions at a higher level, let’s give them a name.
-- 's' for 'State'
-- 'i' for 'Input'
type Layer s i = s -> i -> s
So our update function would look like this:
-- This says update is a function that takes a Model and an Input
-- and returns a new Model
update :: Layer Model Input
And there’s some view function that produces HTML or 3D rendering commands, but we won’t go into that.
view :: Model -> 3DRenderingCommands
To update our state with a list of inputs, we can just fold our update function over the list.
-- foldl' is like 'reduce' or 'inject' in other languages
foldl' update model listOfInputs
Simple Composition
Our model contains two differents states: UIState and GameState. If we assume for a second that each has an update function, we need a binary operator to combine them sequentially:
(*.*) :: Layer s i -> Layer s i -> Layer s i
l1 *.* l2 = \s i -> l1 (l2 s i) i
So, for the example model, if we want both to receive the raw inputs:
updateUI :: Layer Model Input
updateUI model (Tick delta) = ... -- Animate all the things
updateGame :: Layer Model Input
updateGame model (Tick delta) = ... -- Move the enemies around
update = updateUI *.* updateGame
Notice how all three layers have the same type. They are all update functions that produce a new Model given an Input.
Scoping Updates
We would rather define our updateGame function in a way that completely isolates it from the UI state. In other words, we want its type to be Layer GameState Input. We can build a combinator for that using lenses.
liftState :: Lens' s s' -> Layer s' i -> Layer s i
liftState l nextLayer s i = s & l %~ (`nextLayer` i)
‘liftState’ transforms a layer that operates over some state s into a layer that operates over some portion of s, specified by the lens. Assuming we have lenses uiState and gameState that focus on parts of our Model record, our example can become:
updateUI :: Layer UIState Input
updateUI uistate (Tick delta) = ...
updateGame :: Layer GameState Input
updateGame gamestate (Tick delta) = ...
update = liftState uiState updateUI *.* liftState gameState updateGame
To my eye, the individual update functions become simpler, while the main update function carries a little more complexity. This is great! A nasty problem in interactive applications is burying complexity deep into the logic of the program, which causes very subtle bugs. By bringing complexity toward the top level, the lower levels can focus on their individual concerns. This will be a common theme here as we progress.
Changing Inputs
Now we’ll look at changing our input types so that our layers can be even more domain specific. We want the updateGame layer to only take in inputs that are agnostic to the actual controls used. Here’s the combinator we need:
liftInput :: (i -> [j]) -> Layer s j -> Layer s i
liftInput inputmap layer = \s i -> foldl' layer s (inputmap i)
One input at the top layer might generate multiple inputs at the lower layer (or zero), so I have the input mapping function return a list.
data GameInput = GameTick Double
| PlayerJump
updateGame :: Layer GameState GameInput
updateGame gamestate input = case input of
GameTick delta -> ...
PlayerJump -> ...
playerControls :: Input -> GameInput
playerControls input = case input of
Tick delta -> [GameTick delta]
KeyDown SpaceBar -> [PlayerJump]
_ -> []
update = liftState _1 updateUI *.* liftState _2 (liftInput playerControls updateGame)
The updateGame function is now only concerned with types relating to the game itself. It doesn’t know about the UI at all!
Conditionals
Let’s add a pause menu to the game. First of all, we need to disable the game layer when we are paused. when is the combinator for that.
when :: (s -> Bool) -> Layer s i -> Layer s i
when pred layer s i = if pred s then layer s i else s
when takes in a predicate on the state to determine whether to run the layer its given. If the predicate returns false, then we just throw out the layer’s result and use the old state.
To use this in our update function, first we need to add a paused boolean to our Model.
data Model = {
_uiState :: UIState,
_gameState :: GameState,
_paused :: Bool
}
And now our update function becomes:
-- Split into multiple lines for readability
update = liftState uiState updateUI *.*
when (not . _paused)
(liftState gameState (liftInput playerControls updateGame)) *.*
liftState paused (\isPaused (KeyDown KeyP) -> not isPaused)
Note that the example pattern matches on KeyDown KeyP and implicitly ignores any other input. In a real haskell program, you would need to add a catchall handler for the other cases to avoid a runtime error.
Interactive UI
Now, let’s get to the same level as React/Flux and implement a full interaction between UI and Game. So let’s build out our pause menu with actual menu items.
Instead of just a “pause” boolean, as in the previous example, let’s have a MenuState type, which is a pair of a pause boolean and a selected submenu.
type MenuState = (Bool, MenuScreen)
data MenuScreen = MainMenu | Options
data Model = {
_uiState :: UIState,
_gameState :: GameState,
_menuState :: MenuState
}
Now, which menu items are available to be clicked on at any one time? That depends on the current menu state. So we need a combinator that gives us access to the current state.
dynamic :: (s -> Layer s i) -> Layer s i
dynamic layerf = \s i -> (layerf s) s i
What actions could the menu actually generate? We need a type for that.
data MenuAction = NewGame
| Resume
| ChangeScreen MenuScreen
Now we define the submenus and what items are in each submenu.
-- Each sub menu has a list of items, which are the actions
menuItems :: MenuState -> ([MenuAction])
menuItems menuState = case snd menuState of
MainMenu -> [NewGame, ChangeScreen Options, Resume]
Options -> [ChangeScreen MainMenu,
...]
Now we need a layer to handle the menu. It will operate over MenuAction’s instead of raw inputs. So the type will be:
menuLayer :: Layer Model MenuAction -> Layer Model Input
So we’re going to lift our raw input into a MenuAction using the current MenuState. Then once we have a MenuAction, we’ll apply it to both the MenuState and the GameState.
The code will look a bit complex, but that’s just because I threw in some example logic for matching up a click location with a particular menu item:
menuLayer = dynamic $ \(_,_,menuState) -> liftInput (menuAction menuState) menuClickLayer
where menuAction menuState (Click (x,y)) =
let items = menuItems menuState -- get the currently active menu items
-- find which item is hit by our click
index = find (\i -> abs (i * 30 - y) < 15) [0..(length items - 1)]
in [items !! index]
menuClickLayer model menuItem =
liftState menuState updateMenu *.*
liftState gameState updateGameWithMenuAction
updateMenu :: MenuState -> MenuAction -> MenuState
updateMenu menuState menuAction = case menuAction of
NewGame -> (False, MainMenu)
ChangeScreen x -> (True, x)
Resume -> (False, MainMenu)
updateGameWithMenuAction :: GameState -> MenuAction -> GameState
updateGameWithMenuAction _ NewGame = ...
And now here’s our new update function:
update = liftState uiState updateUI *.*
menuLayer *.*
when (not . fst . _menuState)
(liftState gameState (liftInput playerControls updateGame))
Again, you can see that the complex interactions between the different areas are all near the top level and separated from the handlers which are domain specific.
Some Final Words
In my own project I’ve found a particular kind of ‘widget’ useful. It’s essentially a surface with objects on it that you can tap and drag around. You can use it to direct the characters in your game, or implement a level editor, or really anything where you can select and drag things around. So this ‘surface’ is really a function that lifts raw input (clicks) and turns it into higher level domain specific input (taps, selections, and drags). And then you can implement your application logic in terms of selections and drags without having to worry about the raw click input. So basically you can create high level reusable pieces out of this machinery that makes it easy to add new features.
To me, that is the real power, even over something like React. React/Flux only offers you two layers to work in. By composing update functions, you can use as many layers as you want. And each can use their own domain specific state and input.
Now if you want to play around with this stuff, be my guest and check out the code!
https://github.com/asivitz/layer