Passion projects and ruminations related to IT development.
Related
With the simple automata wrapped up we continue with a different class of automata, called “mobile automata”, for which we refer to chapter 3 of New Kind of Science. This article will deal with the implementation insights we gathered along the way.
Compared to simple automata, the mobile class introduces the concept of “states”. States are essentially markers for those cells which we update in a given iteration. The next iteration may have different cells marked as states, determined by the rule. In comparison, with simple automata all cells are updated in each iteration. In mobile automata however, only state cells receive an update. On the one hand this reduces computational time, on the other it introduces more memory requirements, since we need to keep track of which cells are marked as states.
The implication on the implementation will mainly be centered around the following topics:
In addition to these questions this project will see a rampup in unit testing strategies, both on the WebAssembly and the Elm front. With the codebase expanding and requiring frequent iterations, this will help to maintain consistency of result.
As mentioned before, mobile automata are covered in chapter 3 of NKS. in NKS, we distinguish between 3 subclasses of mobile automata:
We look at the combination in the window centered at the state cell to determine its progeny and the next state cell (within the window).
Simple rule example
Similar to simple except the progeny is no longer limited to a single cell but all cells in the window; this implies we shouldn’t have more than one state cell to avoid overlapping windows and thus conflicts in progeny.
Extended rule example
In contrast to the extended case, a single cell is updated but it can introduce multiple new state cells within the window; the number of state cells in the universe can grow/shrink.
Generalized rule example
The Rust implementation was once again straightforward. We already had some structure in the “simple automata” case with a State
trait. Back then this was just empty, but this time around we implement said trait to include a data structure for tracking all cells marked as states. This state object will be passed in the apply
method of the structure implementing the Rule
trait, where it will be read and modified:
Aside from unit testing as usual of code internals, we include more tests in the WASM framework. Given the more complex the automata rules are becoming in the mobile case (progeny & mobility), we opt to cease working with a rule index and instead manipulate progeny and mobility for single combinations of cells directly through our API. This requires mappings between Javascript and Rust types. One example is the Outcome
struct constructor, which serves as a representation of a combination with its progeny and mobility:
For this reason we must have access to specialized test classes. The result can be seen here. Testing can then be performed with
With the API of our WebAssembly code properly testing, we can omit our Rust code from future searches of configuration errors.
Surprisingly, the Elm work took a very long time until I was satisfied with the code. I spent a great deal on the data modeling, trying to figure out what works best and it goes to show there is still a lot to learn about data modeling in a functional language, especially as you strive to make impossible states impossible.
The bulk of the data modeling was focused on the representation of combinations, progeny and mobility. These three combined make up an outcome. When we aggregate all outcomes (one for each combination) they make up a rule. To represent a window of cells (e.g. a combination, but could also be a progeny) we created an opaque Elm type called a Fragment. Internally a fragment has a middle value and two arrays of equal length (denoted as “span”; window = 2*span + 1) to make up the sides next to the middle value.
The symbols making up the cells can be any type, thanks to extensible records:
Whilst one can debate the merits of this type versus a simple List or Array, it does make it slightly easier to use relative indices to access the cell status. If the index is 0, return the middle element, if it is larger than 0, use the index on the right side, if negative, take the absolute value and access the left side of the fragment. The same applies to modification as well.
One could take it a step further and use phantom types to enforce some sort of equal length condition on the sides making up a fragment:
Through our module we can control how these fragments are made and return errors in case of a faulty construction. Functions are included to create all possible fragments of a certain span given a list of symbols to represent the cell values:
With these functions we created a clean interface that can deal with different spans and symbols (+ cardinality). As a sidenote, this is a clear example of some overengineering by design for the purpose of learning, since we use a span value of 0 or 1 and our symbols are either Dead or Live.
The rule module again was a source of a lot of inner contention and although a straightforward way of incorporating all the rule types (simple, extended & generalized) might have been accomplished with a union type:
We opted for a separation of the rule type into its own union type. Though we could do it without, visualization of a rule of a certain type can be made little easier with a union type and an API method to return said type for a supplied rule than extreme generalization of a rule. What I mean by this latter part is that drawing the rule outcomes (combination + progeny + mobility) can still be achieved without knowing what type of rule we are dealing with, purely by looking at the progeny and mobility, but it does move the decision making squarely out of the Rule module, where it ought to be in my opinion. We end up with:
The remainder of the Rule module provides constructor methods for all rule types, some modifiers for the progeny / mobility of rule cases and parameterized encoders and decoders (the user employing the Rule module must supply a union type to represent cell status and a encoder and decoder for that type). As an example, the encoder for a rule is given by:
And in the fragment module:
The user is this obliged to pass in a valueEncoder
.
In our web app for mobile automata, we rely heavily on some specialized data structures (e.g. Fragment, Rule, …) and on JSON decoders and encoders. This incentivizes us to setup unit testing to catch bugs due to small inconsiderate modifications. Test files can be seen here.
With these test files in place we an omit configuration errors from the encoder and decoder logic as well as the Fragment and Rule modules, at least provided all significant cases are captured (the Fragment module for example was missing a test case that would have captured a hardcoded value messing up a high level experiment).
Creating a responsive and intuitive UI to modify a rule for any rule type was more difficult than imagined and in particular small local effects such as highlighting a combination cell when the mouse hovers over it can burden the Elm codebase with a need to keep track of this data in our model and propagating the data back all the way down to the drawing of the individual cell. For highlighting a cell as an illustration we have the following nested data in our model:
To put it to use we define a couple of helper functions:
We use these functions in tandem with an annoying amount of bookkeeping code to propagate it all the way down to the proper view code.
In hindsight we could have used CSS properties to achieve this local effect (thanks to Elm Slack for the tip), omitting the need for propagating this model data all the way down to our view function responsible for drawing a cell and modifying the model when a change occurs. Given our interaction design this may be out of reach but it may be worth the time investment if it means we can slash a lot of code.
One other small change was the usage of the elm-community/typed-svg library for easier construction of SVG elements.
Another difference with the simple automata implementation is how we structure our model and messages. While we are not speaking of a terrible increase in model complexity, our mobile automata web app can benefit from a number of strategies mentioned by Feldman in this excellent talk.
One such strategy is to narrow the types in your functions; as you go down the function ladder (e.g. view -> viewControls -> viewSpecificControl -> …) it is best that the function signatures become increasingly concrete as opposed to passing the entire model down, having the function pick out the part it needs. This helps with debugging and narrowing down the purpose of each function.
Another strategy adopted consists of multiple levels of messages, one top level and several sublevels, depending on which parts of the model we want to modify. To best illustrate this, let’s revisit the model:
Instead of designing a whole host of messages to deal with every variables in the Settings
type, we can provide a top level message, and several mountable submessages:
In our view function, we construct a message by combining the top level and sublevel message. For example in the case of cell resizing we can use UpdateSettings << ResizeCells
.
Because we are no longer using a rule index we can’t paste a number into the URL as a query parameter to represent. When communicating with our custom element we pass a JSON string with the rule configuration. This functionality we can reuse for the URL.
For good reason we won’t be injecting JSON directly into the URL (even though the builder will take care of HTML quoting) and instead opt to encode and compress the data. In our rule update code we use the folkertdev/elm-flate, danfishgold/base64-bytes and elm/bytes libraries to achieve this:
And in reverse for decoding:
The main roadblock in this iteration was not so much Elm itself, we had already tackled all relevant concept in the simple automata case. The main hurdle instead was in data modeling and to lesser extend scalability. The former is often very specific to the application one is building and will require a lot of experimentation on the developer’s part. For the latter we could rely on the worthwhile talks from established figures in the Elm community to guide us to better readability and maintenance of our code.
The resuls can be seen on our Vault.