Recently when I was building development UI for my game, I came to realize that GUI is a better expressiveness test for Dew than a mob AI.
For example, Immediate Mode Graphical User interface (IMGUI) libraries and my game share a same important problem to solve: mapping an identity in user/script code to an internal state in the library/engine. IMGUI libraries solve this problem by requiring the user code to specify an unique identifier, or a tree path of unique identifiers for more complex situations; while in my game, I can use a Dew stream to retain a handle to the internal state.
To test the expressiveness of Dew, I consider a much more complex approach of GUI than IMGUI: building GUI as a hierarchy of components, with all components being retained, and each component’s behaviours being defined locally.
React.js is one popular GUI library with such approach. In this blogpost, I’ll consider a programming model close to React.js, and have the following capabilities defined for components:
There is a global state, which can be accessed by all components;
Each component’s lifetime is determined by its parent;
Each component receives a set of parameters from its parent, and some of the parameters can be time-varying;
Each component can have its own state;
Each component can send back a value to its parent component for feedback, which is often related to user inputs.
Building GUI requires a planned language feature of Dew: channels.
For the current version of Dew, I’ve always a bit concerned that data flow can be somewhat too restricted for some use cases. If wiring data through FFI is not counted, then
at the time when a Dew stream is created, all of its future input must be completely defined, possibly in terms of unknown future user inputs;
also, a Dew stream can only output through by its evaluated/returned value at each frame.
These restrictions are related to the language design that data mutation isn’t allowed. Data mutation across different Dew frames conflicts with the notion of Dew stream, since both of them can represent a varying value over time, and allowing both of them would lead to confusions. Another concern with data mutation is how it works work Dew streams evaluated concurrently.
To solve the problem, I’ve come up with the notion of channels recently. Channels can elegantly solve the problem of more flexible data flow across different Dew streams, while introducing minimal drawbacks.
Channels have two variants, and can be created by calling one of the following two functions:
chan: () -> (''a -> (), *''a) dchan: () -> (''a -> (), *[''a])
When the function is called as
chan () or
dchan () it returns a two-element tuple
(in,out), represents the two endpoints of the channel:
The first element
inis the input/write-end of channel. Writing some value
xto the channel is simply a function call
The second element
outis the output/read-end of channel. To read from the channel, simply evaluate the stream
Note that only two primitive functions are introduced, and we did not introduce any new language constructs. Channel endpoints can be freely passed around like a normal function or stream, and can also be passed through another channel.
The channels look very powerful at this point, but I haven’t yet explained why there’re two variants of them. In our design, channel output endpoints are represented as streams. To evaluate the stream and get channel output, we’ll have to wait for all other streams, because they could all potentially hold the input endpoint, and send some values into the channel during evaluation. This introduces a lot of data dependencies, and would have a big impact on parallelizability of streams.
So instead of the naive version of channels, we split it up into two variants, each with a different restriction to keep data dependency minimal.
With the variant of
chan, we require that exactly one value is sent into the channel, for each frame that the output stream is alive and evaluated. It would be a runtime error if this restriction is violated. With this variant, the output stream is ready to be evaluated as soon as we see the first value sent into the channel, which location of this event is unknown though.
With the variant of
dchan, we do not put restriction on the number of values sent into the channel, but a value sent into the channel may be delayed for a frame. The exact rule for
dchanis a bit complex because we need to work with the notion of “early streams” in Dew, so that it would be possible to process feedbacks collected in the current frame without delay.
Early streams are a distinguished set of streams that are intended to fetch feedback from the outside world between consecutive two Dew time frames. At the beginning of each frame, the runtime has an early phase that tries to evaluate early streams, and the normal phase is after that.
A value sent into a
dchanduring normal phase is delayed to the beginning of normal phase in the next frame; while a value sent into a
dchanduring early phase is immediately available at the beginning of normal phase in the current frame. Output stream of
dchanis always not available during the runtime early phase; but is immediately available at the beginning of runtime normal phase, without dependency on any other stream.
There are other variants of channels possible, but combination of
dchan seem good enough for now, and I may add more later if needed.
I expect that we can build some higher level constructs with the channels, but this is not the focus of this blogpost.
Basic GUI widgets in Dew
To implement this GUI model in Dew, we assume that a context
ctx needs to be serially wired through each component. This context is internal to the GUI library, and will notably contain information necessary for layout. Then a GUI component can be represented as a Dew value of the following type:
*ctx -> (*ctx, *''a)
In this type,
''a is the type of feedback value that the component generates at each frame. A component’s lifetime starts when it’s instantiated by applying a
*ctx value, and ends when the returned
*ctx is no longer referenced. The component’s parameters and states can be stored as the upvalues, even though we haven’t explicitly modeled them.
*''a are two separate streams, because the feedback stream
*''a only depends on the previous value of
*ctx and the current value of user inputs.
We can then define a set of basic GUI widgets:
button: *string -> *ctx -> (*ctx, *bool) label: *string -> *ctx -> (*ctx, *()) textbox: *string -> *ctx -> (*ctx, *string) window: (num,num,num,num) -> (*ctx -> (*ctx, *''a)) -> *ctx -> (*ctx, *''a)
For buttons, the feedback value indicates whether the button is pressed; and for textboxes, the feedback value represents the next text as modified by user inputs.
button can be implemented as follows, and other widgets are similar.
button caption ctx = let ctx' = pre ctx0 ctx in let state' = pre state0 state and state = dew -> button_next_state @state' @ctx in let render = dew -> draw_button @ctx @caption @state in (dew -> let _ = render in layout_button @ctx @caption), (dew -> is_mouse_pressed @ctx')
Rendering code for the button is placed in a separate stream than
ctx, so that rendering for each widget is parallelizable, while
ctx has to be serially wired through each widget.
A widget’s parameter can directly depend on its feedback, as demonstrated as follows:
let textbox' ctx = let newctx, text = textbox text ctx in newctx, text
textbox' is a new widget like textbox, except that its content can only be modified by user input, and can’t be controlled programmatically.
In many cases we’ll want to capture the feedback value of a component when it’s alive, and do something with it, like sending it into a channel. We define a the helper function
getret for this purpose:
# getret : (''a -> ()) -> (*ctx -> (*ctx, *''a)) -> *ctx -> (*ctx, *()) getret f component ctx = let newctx, feedback = component ctx in let aux = dew' -> f @feedback in (dew -> let _ = aux in @newctx), (dew -> ())
Note that an early stream
dew' -> instead of
dew -> is used. This forces the feedback stream to be evaluated and the feedback value to be sent into the channel, as long as the component is retained in the last frame or has just been created. Otherwise, it could potentially introduce cyclic dependence that whether a component should be retained or not depends on its feedback, which in turn depends on the component being retained.
To change the original feedback value, we also define a
# mapret : (''a -> ''b) -> (*ctx -> (*ctx, *''a)) -> *ctx -> (*ctx, *''b) mapret f component ctx = let newctx, feedback = component ctx in newctx, (dew -> f @feedback)
Building up larger components
From basic widgets, we can build up larger GUI components by layout functions. We consider two simple layout functions here:
layout_row: num -> [num, *ctx -> (*ctx, *''a)] -> *ctx -> *() layout_cat: [*ctx -> (*ctx, *''a)] -> *ctx -> *()
layout_row builds up a row of components, with the height of the row specified by the first argument, and width of each child components specified by the first tuple element in the list.
layout_cat builds up a larger component from a series of rows.
These layout functions are static, and does not allow adding, removing or reordering child components dynamically. The child components passed to these layout functions will be instantiated when the larger component is instantiated.
This design is chosen because
*ctx argument must be serially wired by layout functions, so a child component has to be passed as an uninstantiated form
*ctx -> (*ctx, *''a), which makes it unclear what its lifetime is if it’s dynamic. It’s possible to add a unique identifier for each child component in the list to specify that, but for a simple layout function we’ll ignore that possibility.
To give a small example with static-layout components, let’s make an incrementor:
let incrementor = let setbtn, btn = chan () in let state = pre 0 state + (dew -> if @btn then 1 else 0) in layout_row 10 [ 30, label (tostring state); 10, getret setbtn $ button "Increment"; ]
Note that states of a component can be simply created by
pre operator. Global states are also easy to do:
let send_globalfeedback, globalfeedback = dchan () let globalstates_last = pre globalstates_initial globalstates and globalstates = dew -> calculate_globalstates globalstates_last globalfeedback
Now we consider how to add/remove/relayout child components in a larger component dynamically. When we do this across different Dew time frames, some of the old components should be retained, and some new components should be instantiated.
An obvious way to do this explicitly in Dew would be using state machines, which I explained in an earlier blogpost. The retained component instances can be passed among different state machine states as state parameters.
But there’s one problem. When the component among different state machine states, some of the child component instances are retained. So at the moment that a child component is instantiated, it must be given a
ctx stream that could potentially come from multiple state machine states.
To solve the problem, we need to use a channel:
# instantiate : (*ctx -> (*ctx, *''a)) -> *ctx -> (*ctx, *''a) instantiate component = let setctx, ctx = chan () in let newctx, feedback = component ctx in fun ctx' -> (dew -> setctx @ctx'; @newctx), feedback
The channel is created at the moment when the component is instantiated. To supply the channel,
instantiate returns a fake component that intercepts
ctx and send it through the channel.
Now we focus on a simple state machine with only one parameterized state to emulate the style of React.js, and make a simple Todo list GUI as the final example.
# withstate: (''s -> ((''s->''s)->()) -> *ctx -> (*ctx, *())) -> # ''s -> *ctx -> (*ctx, *()) withstate f = let component state ctx = let setstate, statemut = dchan () in let newctx, feedback = f state setstate ctx in (switch $ dew -> if @statemut ==  then @newctx\ else begin let newstate = foldl (fun a f -> f a) state @statemut in \component newstate ctx else), (dew -> ()) in component onclick f = getret (fun c -> if c then f ()) let todolist = withstate $ fun state setstate -> let settext, text = chan () in layout_cat $ lappend (lmapi (fun item i -> layout_row 10 [ 50, item; 10, onclick (fun () -> setstate $ fun state -> lremove state i ) $ button "Remove Item"; ]) state) [layout_row 10 [ 50, getret settext $ textbox'; 10, onclick (fun () -> setstate $ fun state -> lappend state [instantiate $ label @text] ) $ button "Add Item"; ]]
The state is a list of instantiated
label components, and whenever we want to modify the state, we can call
There’s a minor issue with the code, if there’re multiple click events happen at the same, the state mutation function given to
setstate may not be correct. To fix this issue, we can: 1) make sure there won’t be multiple simultaneous user input events; 2) do not mutation the state if it would cause a runtime error; 3) a map instead of a list is used for
state; or 4) we can make
withstate accept only the first state mutation.
In this blogpost, I’ve described a GUI framework similar to React.js. By filling in the missing blanks, I expect that a full-featured GUI library can be built. This GUI library would introduce no unnecessary delay, and has parallelized rendering.
Comparing my framework with React.js, I see two advantages with Dew:
If the goal is build the same ‘component’ abstraction, Dew do not have many advantages. However with Dew I can have much finer-grained abstractions than ‘component’. These finer-grained abstractions are so simple, that I expect much more abstractions are possible.
Data flow and delay are much clearer in Dew than in React.js. In Dew, data flow and delay are explicit; while I wouldn’t expect to understand how React.js works.
It’s interesting to think that React.js turns to use fibers in newer versions, while it’s the default evaluation strategy for Dew programs.
Another popular GUI framework is the Elm language, which was first designed as a way to build web-based GUIs with functional reactive programming (FRP). However unfortunately, they later decided to abandon FRP after it was realized that streams (or signals in Elm’s terminology) does not help much. I suppose this is because the streams in Elm are much weaker in expressiveness than Dew.
By thinking how to do GUI in Dew, I learned a lot along the way:
Channel is indeed a very powerful mechanism for flexible data flow! I only thought of a simpler variant similar to
dchanoriginally, but now better variants are designed, making Dew channels even more powerful.
I wasn’t sure whether early streams should be guaranteed to evaluate when they’ve just been created. Now I think it should, so that they have the property that they’ll always be evaluated when they are alive. Otherwise
getretmay fail to deliver the first feedback value.
Polymorphic variants like those in OCaml are useful to represent feedback events.
stringtype can be useful for building GUIs.
For now, I don’t intend to build development UI for my game in Dew, because that would force me to expose more details of the engine to the scripting facility. But this could be interesting option to explore in the future!