I have below endeavored to supply an amply featured scenario nevertheless minimizing complexity in an inherently somewhat complex assemblage. Please note that defer await
is unique to Swift’s existing defer
. If this is too big an issue, perhaps deferred await
or mark await
(short-hand for “bookmarked await”) may serve.
The opening of the, “Explore structured concurrency in Swift”, talk diagrammed unstructured programming as a confusing mess where control flow could bounce around willy-nilly and even the logic of program execution could fail to be reflected in the order its pieces were transcribed into a code document. defer await
retains most of structured programming’s strictures as well as its top-level document management aspects, merely allowing cross-links within an established hierarchy.
When an asyncDefer
function is called by a defer await
that is located directly above it in the code-block structure, it seems to me that the system will find to be afforded a certain degree of automated error prevention and safety checking.
func someFunc() {
block 1: … // potentially some syncronous code
block 2: defer await deferredFunc(inout arguments) { // supplies the inout arguments with default values that deferredFunc() is prepared to handle (or error-out and return upon failure)
checks & error handling for deferredFunc()’s return
code to execute if/when deferredFunc() successfully returns // may itself further modify resultant inout argument values’ outcomes prior to passing control flow to block 3
}
block 3: // potentially some syncronous code; executes after block 2’s outcome is resolved; block 3 may, contingent upon block 2’s outcome, leap-frog subsequent code blocks known to be potentially dependent upon block 2’s outcome (as may any sub-clauses of block 3 itself that may exist (e.g. via switch case))
block 4: defer await … // straight-line dependency upon deferredFunc()’s inout argument values( and/or their results as further manipulated by blocks 2 and 3)
block 5: // potentially some syncronous code; if block 3’s leap-frog trigger has been thrown then block 5 may perempt any routines it may have which depend upon block 4 (e.g. via switch case)
}
deferredFunc(inout arguments) asyncDefer throws —> T {
step 1 code: … // potentially some syncronous code; may or may not alter or depend upon (and therefore have checks & error handling for) inout argument values as passed-in by someFunc()’s block 2
step 2 code: defer await collabFunc(inout arguments) {
checks & error handling for collabFunc()’s return in this step
code to execute if/when collabFunc() successfully returns in this step
}
step 3 code: … // may or may not alter or depend upon (and therefore have checks & error handling for) collabFunc’s inout argument values as passed-in in its step C; may or may not be syncronous; handles step 2 failing, which may mean returning to the caller, someFunc()
step 4 code: alters inout argument values from step 3 // runs after step 3 completes - also handles step 3 failing, which may mean returning to the caller, someFunc()
step 5 code: defer await collabFunc(inout arguments) {
checks & error handling for collabFunc()’s return in this step
code to execute if/when collabFunc() successfully returns in this step
}
step 6 code: alters inout argument values from step 5 // runs after step 5 completes - also handles step 5 failing, which may mean returning to the caller, someFunc()
step 7: see step 2
step 8: see step 3
// ping-pong as many times as needed; this function may employ a function of its own responsive to these steps’ permutations upon their subject data that adds or subtracts steps
last step: fail-safe back-stop ensuring return to someFunc()
}
collabFunc(inout arguments) asyncDefer throws —> T {
step A: … // may or may not alter or depend upon (and therefore have checks & error handling for) deferredFunc()’s inout values as passed-in in its step 2; may or may not be syncronous
step B code: alters inout argument values from step A
step C: defer await deferredFunc(inout arguments) {
checks & error handling for deferredFunc()’s return in this step
code to execute if/when deferredFunc() successfully returns in this step
}
step D code: … // runs after step C completes - also handles step C failing, which may mean returning to the caller, deferredFunc()
step E code: alters inout argument values from step D
step F: defer await deferredFunc(inout arguments) {
checks & error handling for deferredFunc()’s return in this step
code to execute if/when deferredFunc() successfully returns in this step
}
step G: // see step D (structurally speaking)
// ping-pong as many times as needed; this function may employ a function of its own responsive to these steps’ permutations upon their subject data that adds or subtracts steps
last step: fail-safe back-stop ensuring return to deferredFunc()
}
This concludes the generic pseudocode example.
On the off-chance the thorough reply I had drafted to ahti won’t be seen amiss:
Quite right; my apologies: I have done my best below to articulate a scenario based on the Learn to Code lesson but I am too inexperienced to (at-all reliably) invent from whole cloth scenarios bearing real-world relevance. I will however venture to speculate that scenarios amenable to the limitations of such tightly entwined “bookmark” or “deferred” async/awaits, particularly when nested, could range widely while remaining discreet from optimal application of Task and GroupTask capabilities.
The main problem seems to be that vanilla async is allergic to mutability as it must conform to the Shareable protocol which disallows it in the interest of avoiding race conditions, if I understand correctly. I think I may glimpse a limited context in which safely scoped handling of mutability can be sublimated into straight-line code via keyword convention with attendant compiler help.
I also want to add that discussing the idea of nested instances becomes of necessity quite complex (yet, I hasten to add, still predictable and reliably trackable and traceable)(the following scenario is not nested). I must hope that others who may sense merit in the proposal step-in with potential use cases based in both past real-world experience as well as any coding tac consonant with what I attempt to propose, should they find such to present itself to-mind.
The following image of the playground lesson to which my original post referred shows it requires a Character (green alien), whose jump is just 1 block high, traverse 3 rows of terrain until having gathered an arbitrary number of gems located randomly along them, and which continue to spawn randomly as existing ones are gathered. The flat middle path’s elevation is controlled by an Expert (red alien).
Here is where I diverge from the example in my theorizing, because I’m dissatisfied by simply hiking the middle path to a middle elevation at the beginning of the process and passing the lesson: I want the one doing the work, the character, to be upon whom the task’s completion hinges and that it be upon their direct assessment that it is validly determined to have been successfully completed—it only seems right to me.
So, imagine the following added contingencies necessitating detailed, dynamic collaboration between Character and Expert which neither impels Task-based management nor obliges to conform to what effectively is vanilla async’s serialized analogue to true concurrency:
- the side paths are always, along their entire length, kept too divergent in elevation (above and/or below) for any single middle path placement to grant the Character full access; consequently, the Character now jumps however high they need to, but only while on the side paths (and still only 1 block forward, ever): from/onto the middle path, they still jump just 1 block high, enforcing the Expert’s involvement and that a “figure 8” gathering pattern is the most efficient (which also precludes the need to individually assess side path blocks’ elevations, other than at the end opposite the Character’s latest re-/entry upon the middle path—which would be of the appropriate/next side, obviously)
- the target number of gems gathered can be incremented by an arbitrary amount (within a reasonable range of ~1/2-dozen) at any time*, including between gathering the final gem in the current requirement and assessment of its completion; only the Expert is notified of target increases, mandating discreet communication to the Character (*to prevent even the theoretical potential for an endless loop, no target increase may occur while the Expert informs the Character of one, nor while closing itself out having been told by the Character that they verified their gathered gem total is the current one); the Character’s actions must parsimoniously match the target to be deemed a successful solve, which rules out both mindless full figure 8’s and also a simple
break
once gems gathered ==
the target number and instead forces the parent, the Expert, or both to accept a return value from the Character, obviating any Boolean that could be satisfied independent of the Character’s direct input: the parent requires the actual number of gems at completion, best attested by the Character themselves (if the solution opts for the Character to communicate success directly to the parent then either of them must also inform the Expert so that its deferred async ends along with the Character’s and any of that pair’s influence over and/or claim upon control flow dissolves)(gem target increases have a reasonable chance of being gathered before the next increase to allow the exercise to end in a reasonable time-frame, but there is no hard time-limit)
- upon each of the Character’s returns to the middle path, the side ones may alter their relevant ends’ elevations by arbitrary amounts (kept incompatible with the current middle elevation, as aforementioned) but always less divergent on the end opposite the Character, enforcing the figure 8 pattern as most efficient (that is, forcing the Character to the opposite end of the middle path to continue, efficiently, their gathering)
- only from the ends of the middle path can the Character accurately inform the Expert the direction and extent of elevation change needed
This scenario sets up an inter-depenent back-and-forth, a “ping-pong” (to use the classic telecom metaphor—albeit, comparatively, significantly over-loaded, in this instance), between Expert and Character, whereas the original lesson’s terrain allows for a kick-off 1-and-done from the Expert and a simple “Z/S” gathering pattern from the Character. This enhanced scenario defies both vanilla async and asyncSequence because of the randomness injected by the arbitrary and arbitrarily-timed target increases in conjunction with the demand that Character actions be exactly consummately parsimonious. Yet Task and TaskGroup would be overkill given that the needed collaboration is entirely self-contained. It is only in its tight coupling of Character and Expert that it elides the strengths of tasks, observers, and async (and with it, actors, tethered as they are to Shareable, immutable values). What’s needed is a way to build over time the accomplishment of the task through collaboration between the two, with opportunity to update its details at certain junctures having definite but irregular intervals.
Heuristics yield a few requirements to a solution:
- upon reaching the middle path (which will always be at one of its ends), then gathering gems upon its full length, the Character must supply the Expert with the direction and extent by which to adjust path elevation
- the Character must receive ongoing gem tally target updates no more often than upon meeting the current target, for efficiency’s sake
- upon meeting the target of gathered gems, the Character must verify through the Expert that the target total has not yet/already increased
Sample scenario description:
- Character marches/hops down a row gathering gems
- Character turns toward middle path upon reaching the end of the row
- Character informs Expert of direction and distance to move middle path
- Expert receives Character’s request and fulfills it
- Character steps (or hops) onto the middle path, and turns to walk down it
- side rows’ elevations change
- Character walks to opposite end of middle path, gathering any gems along the way
- Character reaches the end of the middle path and turns to face the other side path
- Character requests appropriate middle path adjustment based on height of next path’s nearest end elevation
- Expert fulfills Character’s request
- Character steps onto side path, turns to walk/jump down/along it, and begins to do so while gathering gems along the way
- 2 blocks forward, the lesson updates the target gems and pings the Expert with it
- Expert pings Character with updated gem target
- Character extends their gathered gem goal to match new target, then continues gathering along its path
This is the core minimum. Once the Character reaches the other side-path’s end, no novel vectors will/can be introduced (other than the fact that the Character’s “half-8” route will be alternately mirrored in completing “figure 8” circuits).
pseudo code:
var targetGems = 15 // arbitrary initial gem target; fed into targetUpdate() via inout
var gemsGathered = 0
let expert = Expert() // a type supplied by the lesson capable of turning a switch up or down to control the middle path’s elevation
let character = Character() // see extended comment immediately below
/*
Character() is a type supplied by the lesson capable of:
* jumping up 1 block (which also moves it 1 block forward) - jump()
* walking forward 1 block - moveForward()
* sensing gems - isOnGem()
* gathering gems - getGem()
* turning left or right - turnLeft(), turnRight()
* sensing if its way is blocked (or is otherwise ended, e.g. empty space with no block to land on) forward and to either side - isBlocked(), isBlockedLeft(), isBlockedRight()
*/
// w/ coordinates and orientation as shown in the screenshot at the top of this post:
world.place(expert)
world.place(character)
[outside code format in this post for easier reading:]
task that randomly triggers targetUpdate() (defined immediately below) via inout (using targetGems), with a reasonable frequency calibrated both to thwart inflexible proscription and yet allow for a relatively high likelihood that within a handful of increments the Character will catch up and end the exercise
// randomly tacks on more gems to the target collection total
func targetUpdate(target: Int){
// hovers in the background and periodically fires itself off at random intervals to add to the gem target
target += # // “#” being a random amount ranging 1 to a half-dozen, say
informExpert(newGemTarget: &target)
}
// these are separate functions because they represent more complex, potentially independent ones in a fuller scenario
// fires when the target gem total is updated, via targetUpdate(), defined previously
func informExpert(newGemTarget: inout Int) { // I’ve opted for the Character to directly inform the parent of success, so this function needs no ability to pass that information to the parent (the parent’s completion will induce the system to close out the expert automatically)
informCharacter(target: &newGemTarget)
}
// propagates updates to the gem target via the Expert to the Character (Character in this scenario cannot “hear” the parent (let alone targetUpdate() ), only speak to it (if Character so chooses—and it has, in this deployment), which it does only after the Expert verifies the total has still not increased beyond what the Character has gathered
func informCharacter(target: inout Int) {
char(latestTarget: &target) // I guess an extension to the Character type could be written elsewhere as gemTotalTrackingAttribute or some such, but the point is that character (lower-case; the instance) maintains a value of its own which mirrors that which targetUpdate(), mediated through the expert and set via this informCharacter() function, dictates targetGems (locally labeled as latestTarget) to be
}
// this is the 1st mover (other than setting the targetUpdate (defined near the top) troll dynamo into motion) and main actor of the exercise (“main actor” in the normal sense, not Swift’s @MainActor)
func exerciseParent(target: inout Int, charGathered: Int) {
if target == charGathered {
return // exits this function, ending the exercise
}
char(gemTarget: &target)
}
// this unifies the acts of updating the gem total and calling charLoop, so that charLoop can be invoked independently of updating its target gem total and vice-versa. This also allows its copy of the top-level gemTarget to be updated discreetly.
func char(latestTarget: inout Int) {
charLoop(gemTarget: &latestTarget, gemsGathered: 0)
}
// the Character’s core gathering routine
func charLoop(gemTarget: inout Int, gemsGathered: Int) -> Int {
while != isBlocked { // basic gem gathering
if isOnGem {
collectGem()
gemsGathered += 1
}
// the success path:
while gemsGathered != gemTarget {
// the following line’s “defer await” call of expert() starts the ping-pong process, looped by the containing while (at least, that’s the idea—I’m not sure I’m coding it quite exactly correctly (this example is pseudo-code, after all!)
defer await expert(gatheredTotal: &gemsGathered) // pings the Expert for input verifying the target hasn’t increased
moveForward()
// more code about managing the turns and how to send middle path height adjustments to expert()
} else {
defer await expert(gatheredTotal: &gemsGathered) // pings the Expert for input verifying the target hasn’t increased
exerciseParent(target: &gemTarget, charGathered: gemsGathered)
}
}
expert(gatheredTotal: &gemsGathered,
}
// places the middle path at an elevation at which the Character can reach the appropriate end block of the appropriate side path
func expert(gatheredTotal: inout Int, currentTotal: Int, elevetionChange: inout Int) asyncDefer -> T {
if gatheredTotal < currentTotal {
charLoop()
}
for i in 1 ... elevationChange {
turnLock // up or down depending on if positive negative
}
}
targetUpdate(target: &targetGems)
exerciseParent(target: &targetGems, currentTarget: