will serve as a means of probing what it feels like to add new features to an already established Elm program; these microcosmic changes will, hopefully, be reflective of maintaining larger applications in Elm. Throughout the course of this article, we will consider successive diffs, starting from the original implementation, while moving toward the final version.
If you would like to see the full source code during any step of the process, please refer to this gist.
Unifying the Model
The first step we must undertake is collapsing the various top level variables, namely cellSize, columns, and rows, into a single Model. We also incorporate the (previously hardcoded) empty Grid into our initial model value. This entire step would be unnecessary, were it not (conveniently) omitted from the original implementation. Generally speaking, it is safer to include extra items in the Model structure, in the off chance they become variable in the future, rather than having global variables. This would be substantially more important when working with modular code but would also vie against common API considerations (i.e. in deciding what to keep private within a module, as opposed to exposing through a public data structure).
Unifying the model is a somewhat messy operation, since it involves changing several type annotations, but this is also very illustrative. We can see how higher level functions deal with the Model type, which flows down to lower level functions, where it is decomposed piecemeal for functions that only need, for instance, grid or cellSize fields. This makes the flow of data through our functions explicitly visible, from top to bottom.
Restarting the Simulation
The first feature we will add is the ability to restart the simulation. This is a fairly straightforward modification, and is the first illustration of the ease with which new features can be added. This is preformed in a few simple steps:
Add a new Restart type case to our Msg union type.
Draw the button in our view functions, configured to generate our Restart message when clicked.
Handle the Restart case in the update function.
This latter point did require the factoring of a separate seed function for generating the command to repopulate the grid from the init function, but that is more corollary than an integral aspect of adding this feature. We also needed to import some additional modules (and expose some additional functions on those already imported).
(N.B. A cursory search regarding the terminology for particular concrete cases of a union type appears to be virtual variant, though this is not part of the Elm vernacular.)
Controlling Grid Dimensions
Now, we come to the point where the initial step of unifying the model will actually pay off. Adding this feature, again, requires changes to the imports section of the program, but otherwise simply adds code without changing any that has already been written. This point demands emphasis: we are able to add entirely new functionality without touching any of the pre-existing code, making our program much more robust to change than many other languages (think about how horrendous adding sliders for controlling the dimensions of the grid in JavaScript could look).
Our additions, much like with the restart button, are of a very particular sort (expect to see this pattern again): update the Msg union type, write HTML to send the new message, and handle the message in the update function.
This particular feature requires an onChange helper method, which is not supplied by the Elm HTML library. This is a consequence of the indeterminacy in the type of values produced by the change event. In this case, we want an integer, but defer handling of parsing the value ourselves until the update cycle—we could perform this type conversion during the initial event handler, but this would be an issue for two reasons. The first of which being the inability to reuse the function easily; we would need a new one if we ever wanted to handle floats. Additionally, if our type conversion fails, for any reason, we are unable to supply a reasonable default value (e.g. the current value in the state of our application).
Another point of interest in the implementation of this simple feature is the use of an additional Dimension union type. A significant feature of Elm, as fas as I am concerned, is this ability to combine union types in such a way that promotes composability. While not inherently obvious from this example, it would be possible to extract a separate Dimension module, exposing this type, and handling various responsibilities related to dimensions of the grid. In the update function, we could, instead of having a nested case statement on the Dimension, call out to a function in our library, thereby reducing the verbosity of the primary update cycle and encapsulating the detailed knowledge of dimensionality.
It is also worth noting that the update function for the UpdateSize branch makes use of the new seed function introduced to handle restarting the simulation. This is done to prevent a mismatch between the values of rows and columns, as compared to the size of our grid, in the internal instance of the Model structure that Elm passes around. For clarity, imagine a situation where the number of rows is updated, but not the contents of the nested list structure: this would have potential to yield nonsensical results, especially in the case where the rows are decreased below their initial value.
Population Density
The ability to change the density of cells in the active state when reseeding the grid fits perfectly within the purview of gold plating, so we should add that feature, as well. The basic pattern, as seen in the previous two examples, remains the same and will not be elaborated upon.
Instead, we should consider how our earlier decision to keep the onChange function generic has allowed its reuse in this implementation. Our new feature, however, does have a much larger footprint than others, since the density field must be propagated through any call to the seed function. An alternative function signature would simply take the model, rather than three fields therefrom, but that is a point which could be argued either way and would not decrease the footprint of this change.
We also rewrite the seed function, in light of its additional complexity, using forward function application, for improved legibility.
Controlling Tick Rate
Changing the speed at which the simulation runs is as simple as can be. Following the, by now, well established pattern, we can do so in just a few short lines. If anything, the use of additional functions from the Time module are interesting.
Mouse Reactivity
Having reached a point where new functionality, of a kind, is as trivial to add as possible, we will now move on to adding a slightly different feature. In this case, we will set any cell moused over to the on state.
This change affects a larger footprint, but is still fairly well contained. We need to pass Int values for row and column through the row and cell functions, such that the latter is able to add them to the onMouseOver attribute for the divs of individual cells.
We also need to write a, rather kludgy setAt helper function, for updating an item at an arbitrary point in our nested list. We will discuss some implications revealed by this, seemingly tangential aspect, shortly
Discussion
At long last, behold the full example before we discuss it in more detail:
This is, unfortunately, somewhat of a mess—not for any fault of Elm. The main problem is having kept everything in a single file in an attempt to make the example self-contained. Imagine separating out the view function (in conjunction with subordinates) and the update (with its helpers) into separate modules, leaving only the core logic in main file. Elm, in fact, is designed in such a way to facilitate (and even encourage) this sort of structure. Additionally, we should have also created a function for generating the range inputs in the view function, but that was also skipped in favor of making each diff atomic.
Now, we return to the earlier point about the setAt helper function. This little nuisance points to a structural issue with this program. Specifically, the use of lists for this implementation is, most likely, suboptimal. Since we often make use of non-sequential access (via valueAt) and, in the final form, updates, we should have instead used Elm's Array module. This should be expected to increase performance, based on the way in which the two different data structures are implemented internally. That said, having to be aware of the implementation details of the standard library of a language is never optimal, but, at a certain point, abstractions at face value can only go so far. Preference for lists over arrays has been discussed before, but the proposal was dropped for lack of actual benchmarks pointing toward their universal superiority, and, as such, this potential optimization is naught but conjecture without further investigation.
One last potential optimization worth exploring is the use of the HTML.Lazy module. In some cursory experiments, excluded from this article, the inclusion of lazy declarations in the various view, row, and cell functions did not noticeably improve performance under strenuous configurations.
Overall, this example has admirably performed its task of giving us context within which to modify an Elm program. As evidenced by the increasingly trivial, practically repetitive, nature of the modifications, it is fair to presume changes will normally follow the same simple pattern of updating union types, dispatching messages via the user interface, and handling these new cases in the update cycle. Some larger changes will necessitate changes to the flow of data through the application (as seen during model unification and the addition of mouse reactivity), but even these changes are mostly local in scope. As a consequence of strong type checking, Elm also ensures, at compile time, that any such cascading change is handled by the developer. From a maintainability perspective, this makes a very compelling case for the Elm programming language.