Francisco Sant'Anna

Menu buttons as local tasks

@_fsantanna

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.