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.