The immediate mode GUI community (including React) has a long history of using optional identifiers to preserve widget internal state. This only works if maintaining the widget’s identifier is easier than storing its internal state in the model. As a result, those libraries defines adhoc rules to offer flexibility in “how unique” the identifiers really are (by implicitly looking at parent identifiers, or draw order, etc). This is worth looking into for inspiration as they have spent a long time thinking about it! I think this works well on small examples, but one has to be very familiar with the rules for more advanced situations involving dynamic collection of widgets (or risk loosing state). React notoriously disallow reparenting a widget for example.
The Elm Architecture clearly cheats on this aspect: a text input is expected to maintain the cursor position / selection, yet this internal state doesn’t have to be passed and updated from the model. It would extremely inconvenient if it did! But since TEA can’t explain how this magic works, user-defined widgets don’t have the same luxury as primitive widgets and so their internal state must leak everywhere.
I find this question super interesting from a purely functional design point of view, and I have a personal take on the subject – which is far from being usable – but there’s a playground tutorial available to experiment with It’s not too far away from what TEA advocates, but the view
and update
function are bundled together to define the type of widgets:
type 'model widget =
{ view : 'model -> image
; update : event -> 'model -> 'model
} (* for some [image] and [event] type defined by the GUI impl *)
This is a bit less straightforward to use than first-order toplevel functions like TEA, but it has the advantage of keeping the view
and update
function in sync automatically. We can combine widgets together, etc, and it’s pretty concise (at the cost of point-free style).
To explain how this allows for inner state in widgets, consider how we would define a “stateful” function in a purely functional way. For example, a function that counts the number of call that were made. Since we can’t do magical side effects to acquire the state, it follows that the previous state has to be passed as an argument and that the new state will be produced as a result:
let view_counter input state =
let output = Printf.sprintf "input is %S and state is %i" input state in
let new_state = state + 1 in
output, new_state
So boring!.. But how do we hide the state from users now?
type ('input, 'output) stateful_function =
Stateful : { state : 'state
; fn : 'input -> 'state -> 'output * 'state
} -> ('input, 'output) stateful_function
This is a GADT, which hides the internal 'state
type (and the initial/current state value). It does expose that this is a stateful function and not a regular 'input -> 'output
pure function. To hide the internal state of such a function, we need to provide an initial value:
let view_counter = Stateful { state = 0 ; fn = view_counter }
We can do some nice things with the internal state type being hidden, like memoïzation in a similar way to SAC/FRP to avoid recomputing a result when the input hasn’t changed:
let memo fn =
Stateful {
state = None ;
fn = fun input cache ->
let output = match cache with
| Some (prev_input, prev_output) when prev_input = input ->
prev_output
| _ ->
fn input
in
output, Some (input, output)
}
Using this idea to represent internal state in a GUI, the type of widgets is actually closer to:
type 'model widget =
Widget :
{ state : 'state
; view : 'model -> 'state -> image
; update : event -> 'model -> 'state -> 'model * 'state
}
-> 'model widget
(And with the same idea, we can memoïze the view
function to avoid rerendering unchanged portions of the GUI.) There are more examples in the playground on what this enables, and how to move/reparent stateful widgets in the GUI hierarchy. Contrary to unique identifiers, I’m especially happy that this explanation of widget’s internal state plays by the same rule as the rest, so one can duplicate a widget and its state, or rollback a widget state history in the same way that one can do all of this easily with the immutable model