Previously, I wrote about Purescript Puzzler, a toy game I wrote to see how the MVI architecture works in practice. A few things kind of bothered me about my formulation of the architecture, some of which is due to the MVI philosophy generally and probably some due to my inexperience with it. A few notable points:
- The model contains all state, even GUI state. This feels like a violation of separation of concerns. Ideally, I should be able keep my model and replace the GUI with a new one if I wanted, with minimal code changes.
- The intent is very thin. I still don’t understand its purpose really. It seems like the goal is to keep the Model distinct from the View by having the Intent work as a translator between the two, but if the Model needs to store GUI state anyway, then there’s not much point.
- Messages are sent between components using an ADT on a single channel between each component pair. Since channels can only accommodate a single type, my ADT needs to encompass every type of action communicated between components. Purescript Puzzler, a minimal application by any reasonable assessment, had 7 actions in its ADT. I can’t imagine how many would be in a decently sized one. The explosion of data constructors in the ADT is also not very compositional; what if I want to build my app out of smaller logical units? Now I need an ADT to wrap the ADTs for each component so I can put them all on the same channel.
- I felt like there was too much display logic in the GUI. I usually want the GUI to consist of reusable components that are configurable externally, and that can’t happen with so much logic in the View. This is my own fault, though. I could have put the display logic in the Model, but that would have coupled the Model even more to the View than it already was.
So I set out to update Purescript Puzzler with a new architecture with the following goals:
- The Model consists of the domain model and nothing more. Specifically, if the user were to save their game during play, the Model consist only of the data that should persist between play sessions.
- The View should consist of reusable components whose rendering and display logic is controlled externally.
- To reduce the possible “explosion” of ADTs as the application grows, communicate functions over the channel instead of ADT messages.
- As an experiment, have the GUI be ‘stateless”. Each GUI is configured with actions that it can perform, and state required to perform those actions is carried in closures.
Overall, I think points 1-3 were pretty successful, but point 4 became a big headache. In the rearchitected version, I renamed the Intent to Controller since it was no longer following the MVI philosophy. Here’s a link again to the playable, original Purescript Puzzler.
Limiting the Model
I don’t like the idea of the Model containing GUI state. To me, the model should represent the “save file” (meaning persistent state) for the application and the operations that can be used to query the model or update the model to a new, valid state. In that sense, model operations should be atomic. I used this example in my previous Purescript Puzzler post.
$ move west 50 yards
This is an atomic action that transitions the model from one valid state to a new valid state. However, in a GUI you might need to select several buttons in sequence to stage the command you want to execute before passing that data to a model function to perform the transition. For example, select Move, select West, enter 50 yards, then submit. The state of the application at the end of each of those actions includes the Model state (which has not changed since the action hasn’t been submitted yet) as well as the transient GUI state. It was this transient GUI state I wanted to keep out of the Model. As a result, previous model items like selectedPiece
and dropTarget
were removed because those were related solely to staging GUI actions (i.e. selecting the piece, then hovering over the board to see where to place it). The result is that my Model only contained clean, atomic actions.
(In retrospect, I think a better approach would be to utilize extensible records to have a Model that operates on only Model fields and a GUI that operates only on GUI fields. Logically, they are distinct, but can populate the same record to be a single source of truth.)
Using configurable View components
In the original Purescript Puzzler, I had code in the View such as this:
case gs.victory of
Nothing -> vtext "Purescript Puzzler!"
Just true -> vtext "You win!!!!!!"
Just false -> vtext "You looooose.... :'("
This code is entirely dependent on the Model being displayed, so the View component can’t be re-used in any other context. Instead, we can pass the behavior logic for the component in as an input, then choose new behaviors as the situation requires. I called this set of behaviors the Spec
, which seems to be fairly common terminology for this kind of pattern. For instance, here is the structure for the GridViewSpec
.
newtype GridViewSpec = GridViewSpec
{ id :: String
, className :: Maybe String
, gridSize :: { r :: Number, c :: Number }
, click :: Callback
, squareClass :: Number -> Number -> Maybe String
, squareFill :: Number -> Number -> Maybe String
, enterSquare :: Number -> Number -> Callback
, exitSquare :: Number -> Number -> Callback
, clickSquare :: Number -> Number -> Callback
, dblClickSquare :: Number -> Number -> Callback
}
The Spec
includes all actions the component can take, including actions to query for required data (like id
) as well as callback actions invoked as a result of user input. In this case, the GridViewSpec
is used to draw a rectangular grid of squares. In Purescript Puzzler 2, GridViewSpec
is used to draw both the puzzle board as well as each selectable puzzle piece, with customized display logic handling each case.
This pattern largely worked well except for a few challenges. One is that I did not define a default Spec
for each component type, so I had to completely define each one instead of redefining parts of the default. This led to pretty verbose code that in most cases merely specified that the functionality wasn’t used. Along similar lines, a Spec
needs to strike a balance between being versatile enough to be useful in many situations, but not so general that typical functionality is difficult to define. Finding that balance can sometimes be difficult.
Another challenge was implementing components that contain other components, such as the area that contains all the selectable puzzle pieces. These containers should generically contain a list of displayable components, so to that end I created a Display
type class with a single function display
that takes an input (the Spec
) and produces a VTree
output. The component container could then contain a list of Specs
with Display
instances, or a list of raw VTrees
, using the instance below.
class Display a where
display :: a -> VTree
instance vtreeDisplay :: Display VTree where
display = id
One side effect of Specs
needing instances for Display
is that all my Spec
types needed to be newtypes instead of type synonyms. This posed some additional annoyances I’ll get to later, but otherwise the ComponentsContainerViewSpec
worked pretty well in terms of displaying a homogenous collection of items. Sometimes, however, it was difficult to update the behavior of a single component in the collection. Also, a useful addition would be some kind of existential wrapper of each component so the container could display a heterogeneous collection of items.
Sending functions over the channel
The most successful development in Purescript Puzzler 2 compared to the original is communicating functions over channels instead of ADTs that represent actions.
Using ADTs, the space of possible actions can grow and grow as the application grows. Additionally, if you want to composed different Models together in a new Model’, you need to declare a new ADT just to wrap the ADTs of each component so that you can put them on the same channel. When received by Model’, the actions are unwrapped and routed to each component for execution and state update. Let’s look at the updateGame
function for the original Purescript Puzzler, which has a monolithic Model.
updateGame (TogglePiece p) s = ...
updateGame (TargetDrop r c) s = ...
updateGame Hint s = ...
...
Notice the pattern? Each has type GameAction -> GameState -> GameState
. So why do we need an ADT anyway? What if, instead, we did:
togglePiece p s = ...
targetDrop r c s = ...
hint s = ...
When curried with the data previously in the ADT, each of these functions has the same type GameState -> GameState
, which takes the state and updates it. As such, we can send any of them on the channel to update the Model. And now, instead of requiring a dummy Init
data constructor to initialize the channel, we can use simple, polymorphic id
. We can even build new functions on the fly to update the game state, so the space of possible actions is limitless without needing to expand an ADT in parallel.
Another benefit to communicating between modules with update functions is that composing different Models together becomes really easy; simply compose the multiple constituent models together into a Model’ and any function from Model' -> Model'
can update any subset of models, check relationships between them, etc.
To unlock this compositional advantage in a convenient way, however, we need to use lenses. One of the great things about lenses is that they allow easy composition of multiple, individual updater functions. For instance, here is an abridged lens from Purescript Puzzler 2 that updates the behaviors of several different visual components at once.
-- first change how the pieces are drawn so border is shown
(_PuzzlerViewSpec..pieces.._ComponentsContainerViewSpec..components .~ ...
)
.. -- Now change hover behavior for board
(_PuzzlerViewSpec..board.._GridViewSpec..enterSquare .~ ...
)
.. -- Now change click behavior of board
(_PuzzlerViewSpec..board.._GridViewSpec..clickSquare .~ ...
)
One annoying thing with using lenses with newtypes is how verbose the lenses become because a lens is required to unwrap the newtype. This is kind of silly since a newtype only has one component that a lens can focus on anyway, so there’s not really any other options. I think a useful solution to this would be to define a type class that defines a newtype wrapper/unwrapper lens. Then use the function (...)
(three dots instead of two) to handle the lookup automatically. The above would then become:
-- first change how the pieces are drawn so border is shown
(...pieces...components .~ ...
)
.. -- Now change hover behavior for board
(...board...enterSquare .~ ...
)
.. -- Now change click behavior of board
(...board...clickSquare .~ ...
)
Note that the trailing ...
after each set operation .~
are indicating removed code in the abridged version, not invocations of the (...)
function.
Using lenses in practice was a big step in my FP skill development, and was pretty easy after deciphering how they work. Their use above still has room for improvement, though.
Carrying GUI state in closures
If sending functions through the channel was the big success with this experiment, carrying GUI state through closures was the big failure.
My original thinking on this issue was to think of GUIs as having no persistent state. Instead, they have a set of actions they can do in response to user input. Each action results in a new set of actions the GUI can perform. At no point does the GUI have data about the “state”, only the set of things it can do.
So, given the above assumptions, how can the new set of GUI actions depend on the previous actions? It can if the actions themselves carry their needed information, either by closure or by currying.
One of the problems with actions returning new actions, however, is that the callbacks for an action need to set the callbacks of the action it returns, and those callbacks need to set the callbacks of the actions they return. It’s callbacks all the way down. Generally, though, the callbacks only need to be defined until you reach some kind of “baseline” state that results when the Model performs an update action. But these baseline states might be a slight modification of another baseline state, which is what you’re trying to define in the first place!
To see what I mean, take a look at this excerpt from my code.
pieceSpec mSel p = GridViewSpec
{ id: ""
, className: if (mSel == Just p) then Just "piece selected" else Just "piece"
, gridSize: { r: rows p, c:cols p }
, click: callback $ const $ send chan $ defer \_ ->
let newSelection = case mSel of
Just sel | sel == p -> Nothing -- unselecting currently selected piece
_ -> Just p -- new selection
in if isNothing newSelection
-- if no piece is selected, return to base spec
then \_ -> controller chan gs
-- else, modify the behavior of the pieces area and board to
-- highlight the selected piece and enable drop preview
else
-- first change how the pieces are drawn so border is shown
(_PuzzlerViewSpec..pieces.._ComponentsContainerViewSpec..components .~
A.map (pieceSpec newSelection) ps
)
Look at the first line and the last line of the code. The specification of actions for the selectable piece pieceSpec
defines the resulting set of available actions from a click event as a function of a new pieceSpec
using the newly selected piece! This recursive definition would be right at home in Haskell, but the strict evaluation of semantics of Purescript won’t fly here. I had to “lazify” this code using the purescript-lazy
package to defer its evaluation until it is actually called by the onClick
event. This was my first clue that this approach wasn’t going to work too well.
Another problem with carrying GUI state in the action closures is that there is no longer one source of truth. For instance, if you pass the GameState
to two different closures, there are two different version of truth, so updating one closure with new GameState
will not automatically update the other. This was an issue in Purescript Puzzler 2 when the user has already selected a piece and wants to remove another from the board before placing the selected piece. The selection action binds the current game state to the closure when a piece is selected so the user can see what placements are legal and which are not. However, if the game state changes before the piece is placed (like by removing some other piece from the board), the old game state is still bound to the placement validity check, so valid moves might be shown as invalid. Really, it became kind of a mess, and in some cases I just left features from the original Purescript Puzzler unimplemented to avoid the hassle.
Yet another problem with using closures to record GUI state is that it’s never clear which objects are included in the closure and which aren’t. Is it possible that some objects are never garbage collected because they are always included in a closure in one way or another? I think it’s definitely possible. Toward that end, it’s much harder to inspect the environment included in a closure than it is to just inspect the state of a record in memory, so debugging is more difficult.
Next steps
Purescript Puzzler 2 was a success in that it informed what techniques in FP architecture might work and which won’t. If there’s ever a Purescript Puzzler 3, I’d include the following attributes:
- The Model can indeed include all domain model state as well as all GUI state (unless GUI state is stored in GUI components like in React), but extensible records should be use to logically separate the two.
- Communicate updater functions through the channel instead of using ADT messages. This was a nice improvement.
- Use lenses to define update relationships between components, but find some way to ameliorate the pain of newtype unwrapping.
- Try out a GUI framework like React. Virtual-dom works well for a from-scratch approach to a GUI, but I know of zero libraries of reusable virtual-dom components, whereas there are several React component libraries available.
That’s all, folks. Happy Valentine’s Day!