There is standing advice not to store functions in the model (or msg). However, when designing particularly package APIs this advice can be tricky to follow, particularly when we want to allow the user to specify some stateful computation that our library will evaluate. As a running example I will provide a simulation of physical forces.
In this example, the user will specify a collection of objects (Entity
) that have an identity (comparable
in the examples below), position and velocity. They will also configure our library with a collection of forces that they would like to have simulated. But here comes the tricky part: how do we specify what a force is? Conceptually it is something that takes the positions and velocities of the objects and modifies those velocities in some way, possibly based on some parameters.
type Force comparable
-- modifies objects to have a global average position at these coords
= Center Float Float
-- simulates rubber bands between nodes
| Links (List {source : comparable, target: comparable, strength: Float}))
-- simulates charge/gravity with a strength param
| ManyBody (Dict comparable Float)
The above representation is good because it obeys the advice given, but isn’t terribly flexible. If a user wants a force to behave slightly differently, the only way is to fork the library. We might be tempted to rather do:
type alias Force =
List Entity -> List Entity
center : Float -> Float -> Force
center = -- implementation
...
However, this throws out all the advantages that the advice gives us: serializablity, debugabillity, etc. Additionally, storing our model as data would allow us to potentially make certain optimisations - perhaps we can make a many body implementation that centers as a side effect and merge those two?
For a long time I have been caught between these two (indeed elm-visualization uses the first approach).
However, recently I realised that one can have their cake and eat it too, or rather provide a sensible compromise:
type Force comparable
= Center Float Float
| Links (List {source : comparable, target: comparable, strength: Float}))
| ManyBody (Dict comparable Float)
| Custom (List Entity -> List Entity)
This allows the typical user to use the provided functions and reap all the usual benefits of no functions, but allows the advance user to trade away those for unlimited flexibility.
As an aside: this only arises sometimes, as in many cases you can simply let the user provide the function as an argument and you do not need to store it. However, this is not always practical – in this example the forces need to be computed from the nodes, which are often dynamic in nature, and so the forces would need to be re-computed on each frame. This would be too inefficient.