
In the previous post, we discussed the outermost code to alternate between the main menu and the chosen screens:
-- enumeration with the possible menu choices
type Menu = <Story=(), Editor=(), ...>
-- task signatures for the menu and buttons
task main_menu: () -> Menu 👈 (this post)
-- returns the chosen screen to navigate
task menu_button: [pos:Point, lbl:String] -> () 👈 (next post)
-- receives a position and label to show
-- spawns the game code
spawn {
-- the outer loop
loop {
-- main menu
var opt = await spawn main_menu ()
-- chosen screen
var lbl = ifs {
opt ? Story { "Story" }
opt ? Editor { "Editor" }
... -- other options
}
await spawn menu_button [[0,0], lbl]
-- loops back to menu after screen terminates
}
}
-- enters the SDL engine loop
call pico_loop ()
Following the top-down approach, in this post, we discuss the main_menu and
leave the menu_button implementation for the next post.
The main_menu task spawns a set of menu_button tasks in parallel (1️⃣), each
one corresponding to a menu option.
The menu then returns the clicked option to the outermost code as an
enumeration (2️⃣):
task main_menu: () -> Menu {
par {
await spawn menu_button [[-125,35], "Story"] 1️⃣
return Menu.Story 2️⃣
} with {
await spawn menu_button [[ 125,35], "Editor"] 1️⃣
return Menu.Editor 2️⃣
} with {
... -- other options (1️⃣,2️⃣)
}
}
The main_menu task expects that a menu_button terminates when it is clicked
(1️⃣) in order to return the corresponding enumeration (2️⃣).
Like in the outermost code, we use the direct style of spawn-await to nest
arbitrary tasks.
Here, we also use the par composition whose branches expand to anonymous
tasks that capture their enclosing lexical context.
For instance, note that the return terminates the enclosing main_menu as a
whole, disregarding the anonymous task between them.
I believe that this kind of mechanism is not possible in programming languages
that provide structured concurrency exclusively as a library.
Finally, the most relevant structured mechanism in this code is how Ceu handles
the lifespan of tasks.
A task is like a local variable, i.e., its lifespan is attached to the block
enclosing it.
In the main_menu, each par branch spawns an anonymous task, and each
anonymous task spawns a menu_button task.
Hence, the menu as a whole handles at least 2x tasks for each menu option,
which are all active at the same time.
When a return is reached, it escapes all blocks in the parent task, aborting
all nested tasks automatically.
This hierarchy of tasks is one of the control-flow patterns described in the
previous post:
The original implementation in C++ relies on an explicit container to hold
the buttons, which are destroyed together with the menu object.
In comparison to local tasks, the main drawbacks of object containers are that
(1) all nested objects share the same lifespan of the container, and
(2) the lifespan of the container matches the lifespan of its enclosing
object.
As a result, lifespan hierarchies in containers cannot be fine grained unless
they rely on manual management (e.g., through add and remove method calls).
Comment on
@_fsantanna.