Exploring the MVI architecture with Purescript Puzzler

Purescript Puzzler is a simple puzzle game using Tetris-like shapes that I created for the sole purpose of

  • Practicing purescript generally.
  • Trying out the purescript libraries virtual-dom and signal.
  • Exploring FRP and the Model-View-Intent style of application architecture.
  • Feel out the workings of possible wrapper libraries to make the process easier.

The game is playable on github, but as configured is pretty difficult to solve, so I recommend using the Hint button liberally to adjust the game’s difficulty level to your tastes.

Below is a collection of my impressions of the experience in making this game. My goal here is not to argue for or against any of these libraries or techniques but simply to write my ideas down while they are still fresh in my head and invite discussion.

Approach Overview

Before talking about my impressions, I’ll briefly describe the design of the game from a high-level viewpoint.

The easiest place to start is with the View. The View accepts the entire game state as an argument and produces a VTree representing the desired DOM state used for display. The View itself has no internal state aside from some stateful operations that can be performed on the DOM when the VTree is actually rendered. Also passed as an argument to the View is a Channel from the signal library. The Channel is used in event handlers to send lightweight messages to the Intent that descibe the user’s interaction. For example, when the user selects a piece to place on the board, the View sends a PieceClicked Piece message on the Channel to describe the event (what happened and what it happened to).

The Intent’s purpose, according to the MVI blog post linked above, is to translate user input events into Model events. In my implementation, this was really simple and there was essentially a 1-to-1 correspondence between my ViewEvents and my GameUpdate events, which seems like a code smell.

The Model contains all the game’s state GameState. This includes both the “domain model” state as well as any GUI related state. The Model also provides an update function that consumes GameUpdate messages and returns updated GameState.

The individual components are wired together in Main. A Channel is created for communicating ViewEvents. The Channel is subscribed to to produce a Signal ViewEvent. This is fed to the Intent, which produces Signal [GameUpdates], which are folded over with the Model’s state update function to produce Signal GameState. This signal is then fed into the View to create a Signal VTree, which is rendered with an effectful rendering function.

The Intent produces a Signal [GameState] instead of Signal GameState because I figured, in the general case, a single event on the View might trigger multiple updates in the Model, but in that wasn’t the case in this game.

Signal library

The signal library has some quirks, but overall it’s really nice. It’s essentially a clone of Elm, which has been exploring the concept of FRP related GUIs for a long time, so a lot of the kinks have been worked out.

Signals defined for all time

Signals are defined for all points in time, including time zero. Signals of “events,” therefore, aren’t so much discrete events in the usual sense. Instead, they represent the most recent event to happen. A consequence of this is that signals require initial state. There’s some discussion already here.

This formulation occasionally presented me with pain points when using signals. For instance, in order to get notifications of DOM events, I needed to put a Channel in my event handlers. In most cases, this isn’t a problem, but consider events that fire rapidly like drag events (drag and drop was my first approach for Purescript Puzzler, which I abandoned after great difficulty). Usually I won’t want to update the model on every drag event, so I can use sampleOn (every 40) at the subscription site so that drag events are limited firing at a 40 ms rate. But wait…what if dragging stops? No new events are put on the channel, but I’m still sampling the most recent event at 40 ms! Gah! A similar situation as this caused a nasty bug in which a “toggle” event kept firing, causing a div border to flash at 80 ms period. I worked around this by combining my sampling with distinct' to eliminate samples that didn’t change, but it does show that constant-time signals can be tricky to think about.

Another nit is that a Channel must be initialized with some value for time zero, but if a channel is being used to send events, how do I initialize when no events have happened yet? I had to create a dummy event called Init, that did absolutely nothing and had no significance, just to give the channel some kind of value. It’s possible that with some smarter designing, I could order the MVI components in such a way that the initial value to start the chain is the initial GameState, eliminating the need for Init, but it is not obvious to me whether that would work out.

Trickiness with effectful Signals

I had a lot of problems trying to figure out a way to process signals in an effectful way that also incorporated state. Specifically, I wanted my render function to add the “first” VTree to the DOM and after that patch the DOM with the diff between previous and current VTrees. However, I couldn’t figure out a way to use the signal combinators to achieve this. Instead, I had to provide an initial, dummy VTree, render it, then use the rendered node and the previous VTree as internal state that I folded over with new VTrees.

Signal distinction

Even though I got bit by the rapid sampling problem I mentioned above, it was pretty easy to fix once I understood it. The ability to apply distinct' for reference equality or distinct for value equality is pretty awesome, and underscores how much power can be packed into a small set of well-designed combinators. It is something the developer needs to think about, however. For instance, typically I used the distinct' variant out of habit, but in doing so I had to do things like

myFunc state = 
  case state.value of
    Nothing -> state
    Just v -> state {value = Nothing}

Functionally speaking, the two lines have the same result: a state with value set to Nothing, but in the first case, the result is referentially the same as the input, but in the second case it’s not; a new state will be built and returned. I think it the future, it would probably be better to simply use the value equality variant of distinct and only bother with the referential version if it was required. However, the value variant requires an Eq typeclass, so I can’t use them on type synonyms (as of now). I use type synonyms for records pretty liberally, so actually I’m not sure it would even work with the above code.

(I’d love to have view patterns to simplify code for cases such as above.)

Interactions “set in stone”

Once a signal line is established, I don’t think there is any way to “unsubscribe” from signal line updates. Any signal sent through a function will be called when that signal updates, so the function itself needs to handle its own logic for listening to or ignoring the updated values.

It’s also not clear to me how I would listen to multiple signals on the fly, adding new signals as needed. I expect it to be possible; I just don’t have any experience with it and feel like it could be a pain point.

Virtual DOM

Overall, virtual-dom is really cool to use. Writing a declarative GUI without having to worry about update-this or change-color-that was a straightforward, easy experience. I never have to worry about keeping the GUI and game state in sync because I’m building the GUI from scratch at each update. A cool side effect of this is that I can save my Model state at any time, then load it up and instantly see exactly what I was looking at when I saved. A stateless GUI makes reasoning about the display really straightforward.

For small projects, I wouldn’t hesitate to use virtual-dom again. However, for more complex ones, I have some concerns.

Room for VDOM wrapper libraries

The virtual-dom bindings for purescript are pretty low level, and that’s the way I think they should be. In JS land, the virtual-hyperscript DSL can be use to easily set up VTrees and includes a lot of convenient features out of the box, but it is opinionated. I’d love to see a similar purescript library and will probably make one sometime in the future.

One thing I want to avoid, however, is over-generalizing by writing a virtual-dom interpreter for a general DOM declaration language. Doing so might make common use cases easy but virtual-dom specific cases hard.

Stateless GUI is a double-edged sword

Not worrying about GUI state is a huge relief on the GUI side, but that means that every single stateful component in the application is now included in the Model – open dialogs, mouse location clicks, you name it. Purescript Puzzler is pretty simple in both the model and the GUI, so this is manageable. However, I can’t help but feel that in the general case, this actually couples the Model to the GUI more instead of less. Now the Model needs to know about all the little components on the GUI side in order to capture their state. This kind of formulation does not lend itself well to composition or separation of concerns.

Along the same lines, one thing I’m confused about in the MVI style is the purpose of Intent. All mine did was act as a message translator, but I think it might make sense to capture some state in Intent. One use case might be to evaluate the sequence of users actions before translating them for the Model.

As an example, imagine a text-based adventure game in which the user can move around like this:

$ move west 50 yards

This can be represented in a single Model event very easily, but what if this were a GUI? Let’s say the user clicks the Move button, then clicks West, then types in 50, chooses yards, then hits enter. While cumbersome, a GUI like this should be possible. Including some internal state in the Intent component would allow each input event to be collected into a single message to be sent to the Model. However, because there are no Model updates after each input event, there is no feedback that the user did anything! There’s not a straightforward solution, which is why the MVI blog post claims this is still an open problem.

Animations are inherently stateful

Transitions and animations are part of the polish in a really nice UI experience, but they are inherently stateful. All of that state (position, orientation, size, alpha, etc.) needs to be captured in the Model, needlessly complicating it.

One away around this is to use virtual-dom hooks to change node properties on “nextTick”. The change in properties will trigger a CSS transition, which will handle all the animation state behind the scenes. I haven’t tried either of these approaches, so I can’t comment on them extensively, but it definitely feels right to keep a bunch of pointless GUI state out of the Model.

Full VTree diff each update

If I understand the internals of virtual-dom correctly, the entire VTree is diffed and patched at each update. Typically, only a small part of the tree will change, but the whole tree is diffed all the same. There are some implemented shortcuts for reference equality of nodes, but in straight-forward functional style, the entire VTree will be rebuilt from scratch without any kind of caching, so reference equality will never hold. I wonder what the performance limit on this strategy is – how big can the VTree be and how fast can it be updated before this is a problem?

Virtual-dom uses Javascript conventions

Virtual-dom uses a number of JS style conventions that present some impedance when trying to use them from Purescript. The biggest is probably the use of arbitrary records that define id, attributes, style, etc. These are pretty verbose and annoying. In fact, most of the work done by the virtual-hyperscript DSL is simply building these records for the user in an easier way. However, in Purscript, each record is technically a different type, which doesn’t always jive well with the type system.

Another JS convention is the use of undefined to remove properties from nodes. This only affected me in one case in which I needed to disable/enable the Hint button. To disable/enable, I use the following attributes.

{ attributes: { disable: "" } } 
-- presence of disable attribute disables button, regardless of value
{ attributes: {} }              
-- absence of disable attribute enables button

The problem is that the above two records have different types, so I cannot return one or the other from an if clause. Instead, what I did is use FFI to create def and undef variables, and assigned disabled with one or the other as appropriate. An alternative would be to return entirely different VTrees from the if/else clause instead of trying to select only their options.

Overall

Building Purescript Puzzler was a great learning experience, especially since I’m not the most experienced web dev. I sunk my teeth into a few cool libraries and got my first real taste of FRP outside of toy examples. However, I still feel like thar be dragons lurking in the shadows with this approach, and the apparent lack of complex examples is not reassuring. I think part of the problem with this is the absence of wrapper libraries that make common tasks easier, which inhibits exploration. Another open problem is handling of complex GUI state within the Model. I’m also concerned with integrating foreign GUI components (“widgets” in virtual-dom parlance) and persisting state across them.

The approach does, however, eliminate sync issues between the components, which naturally eliminates a large source of bugs.