Bookmark async

Given async is syntactic sugar for what are actually tasks under the hood, and the fact that Swift 5.5’s async cannot ping-pong data between the same 2 functions more than a 2-hop without establishing an endless loop (which happened when I tried with inout variables):

I propose an unstructured or “bookmarking” async which dog-ears code execution after having initiated an async call, syncrony holding for any remainder past that clause, but once the async function has completed and returned its result, then the rest of the calling clause executes only at that point. This would obviate explicit task delimitry within a calling clause’s scope when results must be built up through several inter-dependent steps none of which need exposure beyond the call clause’s scope (which is also why nested bookmark async’s can’t create a runaway cascade, if I theorize correctly). (All the same cautions regarding state change upon async returns continue to apply.)

This would expand the power of async while preserving its safety, seems to me.

“try await” later in the parent calling function could still safely rely upon the results of a long ping-pong sub-process it initiated earlier in its code while remaining syncronous.

Disclaimer: I am a brand-new aspiring programming only to the final lesson in the Learn to Code 2 playground’s Properties chapter, but after trying to think this through as carefully as I could and looking for alternative approaches I think there’s room for my idea as something uniquely efficient in its domain of applicability.

It would be very helpful if you could illustrate your problem and proposed solution with a few code examples.

2 Likes

This is the most confusing post I've ever read.

1 Like

My terminology is from this year’s WWDC talks:

  • Meet async/await in Swift
  • Meet AsyncSequence
  • Protect mutable state with Swift actors
  • Explore structured concurrency in Swift
  • Swift concurrency: Behind the scenes

but I’ve flagged this whole thread for deletion since I’m obviously not coming across at all, and I don’t know that I can explain myself intelligibly to the standard required without more under my belt.

As suggested a code sample with comments if needed goes a long way.

1 Like

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:  

IIUC, what you're talking about is a well-established concept called "co-routines", where two functions can "yield" values to each other, producing — potentially — that ping-pong effect.

However, Swift doesn't have co-routines as a language feature right now (although async/await uses a co-routine execution model internally). There are certainly use cases where co-routines fit the problem very naturally, but in Swift you're going to have to find a different solution.

1 Like

I appreciate the effort you’re putting into these posts however I’m afraid they are extremely convoluted and difficult to understand. There’s just too much noise to make sense of it. Please try to simplify your request to a single paragraph and a code example no more than a few lines showing the desired syntax / behavior. If you find yourself writing more than that, start over and simplify again. Do not expect your readers to parse and understand an entire program and it’s goals to explain a simple piece of syntax.

1 Like

You do correctly understand (however incompletely): those would seem to be a fairly minor subset of capabilities defer await might pose, but it’s allright, I’m not going to try to push the idea further, for now✌️

That’s just the trouble: to be powerful enough to justify its level of addition to the language, it would have to be versatile, impossible to exhibit in very simple examples, all the more so due to its exhibiting its distinguishing strengths primarily in the unfolding of several components’ interactions across a fair stretch of operation​:person_shrugging::sweat_smile: I’m laying this thread to rest​:v: