coroutine(n)

πŸ“¦
version 9.0.3 Β· 2 versions
, in alpine 3.23, alpine 3.24, fedora 43, fedora 44, nixos 26.05

πŸ’– coroutine(n) β€” Tcl Built-In Commands πŸ’–

okay bestie, sit down, hydrate, because this is the Tcl coroutine page and it is lowkey one of the most iconic things in the whole language. it lives in the Tcl Built-In Commands manual section (that's the (n), iykyk) and it covers the whole squad: coroutine, yield, yieldto, coroinject, and coroprobe. πŸ¦‹


✨ NAME ✨

coroutine, yield, yieldto, coroinject, coroprobe β€” create and produce values from coroutines πŸ’…


🌸 SYNOPSIS 🌸

coroutine name command ?arg...?

yield ?value?
yieldto command ?arg...?
name ?value...?

coroinject coroName command ?arg...?
coroprobe coroName command ?arg...?
see those lil' 2 marks floating around this page? that's just the manpage flagging the newer bits. no cap. πŸŽ€

πŸ‘‘ DESCRIPTION πŸ‘‘

🧚 making a coroutine

so coroutine creates a brand-new coroutine context (and it gets its own associated command, which is the whole point) named name, and then it immediately executes that context by calling command, passing in all the other remaining arguments with no further interpretation. no extra vibes, no re-parsing, just straight through. ✨

and the moment command returns normally, or bails out with an exception (an error, say), the coroutine context name gets deleted. gone. poof. πŸ’€

πŸ“ yielding, the main character moment

inside that context you can generate values as results using the yield command. if you don't hand it a value, the empty string is what you get. when yield gets called, the context suspends execution and the coroutine command returns whatever argument you gave to yield. 😌

then you resume it by calling the context command β€” and you can optionally pass in one single value, which becomes the result of the yield call that caused the suspension in the first place. one value. that's it. πŸ’…

and if the coroutine context never yields at all and just returns the normal way? then the result of the coroutine command is the result of evaluating the context. simple as.

🌈 yieldto, aka passing the aux

the coroutine can also suspend by using the yieldto command. but here's the plot twist: instead of returning, it cedes execution to some command called command β€” which gets resolved in the context of the coroutine β€” and you can pass it any number of arguments you want. 🫢

since every coroutine has a context command, yieldto can transfer control directly from one coroutine to another. (which is only advisable if the two coroutines are expecting that to happen, bestie β€” do not surprise them.) but honestly any command can be the target.

if a coroutine gets suspended by this mechanism, you resume it by calling the context command and optionally passing in an arbitrary number of arguments. the return value of that yieldto call will be the list of arguments passed to the context command, and it's up to the caller to decide what to do with those values. your call, your problem, your era. 😭

the recommended way of writing a version of yield that allows resumption with multiple arguments is by using yieldto and the return command, like this:

proc yieldMultiple {value} {
    tailcall yieldto string cat $value
}

obsessed. ✨

πŸ¦‹ deleting, naming, and traces

the coroutine can also be deleted by destroying the command name. and you can retrieve the name of the current coroutine by using info coroutine. πŸ”

if there are deletion traces on variables in the coroutine's implementation, they will fire at the point when the coroutine is explicitly deleted (or, naturally, if the command returns conventionally). so like, they do fire, just on that schedule, not earlier. noted. πŸ’œ

πŸ’« namespace tea

at the point when command is called, the current namespace will be the global namespace, and there will be no stack frames above it (in the sense of upvar and uplevel). however β€” and this is the part people get wrong β€” which command to call is determined in the namespace that the coroutine command was called from. two different things, bestie. 🌸

πŸ”₯ coroprobe β€” peeking at a suspended coroutine

a suspended coroutine (i.e. one that has yield-ed or yieldto-d) may have its state inspected (or modified!) at that point by using coroprobe to run a command right where the coroutine is sitting. πŸ‘€

the command takes the name of the coroutine to run the command in, coroName, and the name of a command (any any arguments it requires) to immediately run at that point. the result of that command is the result of the coroprobe command, and the gross state of the coroutine remains the same afterwards β€” i.e., the coroutine is still expecting the results of a yield or yieldto as before β€” though variables may have been changed. so the vibe is untouched, the variables are not. πŸ™ˆ

🌟 coroinject β€” sliding a command in

similarly, the coroinject command may be used to place a command to be run inside a suspended coroutine (when it is resumed) to process arguments, with quite a bit of similarity to coroprobe. however, with coroinject there are several key differences:

  • the coroutine is not immediately resumed after the injection has been done. a consequence of this is that multiple injections may be done before the coroutine is resumed. the injected commands are performed in reverse order of definition (that is, they are internally stored on a stack). last in, first out, periodt. πŸ₯ž
  • an additional two arguments are appended to the list of arguments to be run (that is, the command and its args are extended by two elements). the first is the name of the command that suspended the coroutine (yield or yieldto), and the second is the argument (or list of arguments, in the case of yieldto) that is the current resumption value. πŸ“Ž
  • the result of the injected command is used as the result of the yield or yieldto that caused the coroutine to become suspended. where there are multiple injected commands, the result of one becomes the resumption value processed by the next. chain reaction, slay. πŸ”—

the injection is a one-off. it is not retained once it has been executed. and it may yield or yieldto as part of its execution. one and done, no leftovers. πŸ’…

⭐ the big rule about running coroutines

note that running coroutines may be neither probed nor injected; the operations may only be applied to coroutines that are suspended. (if a coroutine is running, then any introspection code would be merely inspecting the state of where it is currently running; coroinject/coroprobe are unnecessary in that case.)

so like, don't try it. it's not a thing. πŸ›‘


to be continued, bestie β€” more of this page is coming, don't go anywhere πŸ’–

πŸŽ€ EXAMPLES πŸŽ€

Okay bestie, this is the part where it all clicks. Buckle up. πŸ’–


Example 1: the never-ending number machine

This one is a coroutine that spits out an infinite sequence of even values, plus a loop that eats the first ten of them.

proc allNumbers {} {
    yield
    set i 0
    while 1 {
        yield $i
        incr i 2
    }
}
coroutine nextNumber allNumbers
for {set i 0} {$i < 10} {incr i} {
    puts "received [nextNumber]"
}
rename nextNumber {}

See that rename nextNumber {} at the end? That's how you send a coroutine to its final rest. Gone but iconic. πŸ’…


Example 2: the accumulator that just keeps adding

Here the coroutine's whole job is adding up the arguments you hand it.

coroutine accumulator apply {{} {
    set x 0
    while 1 {
        incr x [yield $x]
    }
}}
for {set i 0} {$i < 10} {incr i} {
    puts "$i -> [accumulator $i]"
}

Example 3: the Sieve of Eratosthenes, but make it coroutines

Classic prime-finding algorithm, coroutine edition. And yes β€” there are coroutines being born inside a coroutine. Nested. Recursive. Main character energy. πŸ‘‘

proc filterByFactor {source n} {
    yield [info coroutine]
    while 1 {
        set x [$source]
        if {$x % $n} {
            yield $x
        }
    }
}
coroutine allNumbers apply {{} {while 1 {yield [incr x]}}}
coroutine eratosthenes apply {c {
    yield
    while 1 {
        set n [$c]
        yield $n
        set c [coroutine prime$n filterByFactor $c $n]
    }
}} allNumbers
for {set i 1} {$i <= 20} {incr i} {
    puts "prime#$i = [eratosthenes]"
}

Example 4: the juggler β€” passing values around three besties

Three coroutines yielding to each other, tossing a value back and forth like it's a hot potato. Larry, Curly and Moe are literally in the code. I cannot. 😭

proc juggler {name target {value ""}} {
    if {$value eq ""} {
        set value [yield [info coroutine]]
    }
    while {$value ne ""} {
        puts "$name : $value"
        set value [string range $value 0 end-1]
        lassign [yieldto $target $value] value
    }
}
coroutine j1 juggler Larry [
    coroutine j2 juggler Curly [
        coroutine j3 juggler Moe j1]] "Nyuck!Nyuck!Nyuck!"

Example 5: peek inside AND poke the input

This one's a simple coroutine that collects non-empty values and returns a list of them when you don't give it an argument. It also shows how we can look inside the coroutine to find out what it's up to, and how we can modify the input on a one-off basis. 🌸

proc collectorImpl {} {
    set me [info coroutine]
    set accumulator {}
    for {set val [yield $me]} {$val ne ""} {set val [yield]} {
        lappend accumulator $val
    }
    return $accumulator
}

coroutine collect collectorImpl
collect 123
collect "abc def"
collect 456

puts [coroprobe collect set accumulator]
# ==> 123 {abc def} 456

collect "pqr"

coroinject collect apply {{type value} {
    puts "Received '$value' at a $type in [info coroutine]"
    return [string toupper $value]
}}

collect rst
# ==> Received 'rst' at a yield in ::collect
collect xyz

puts [collect]
# ==> 123 {abc def} 456 pqr RST xyz

coroprobe lets you peek at what's happening inside, coroinject lets you sneak in a replacement for one single exchange. iykyk. πŸ¦‹


🧠 DETAILED SEMANTICS 🧠

This example proves two things: coroutines start out from the global namespace, and command resolution happens before the coroutine stack gets created. No cap.

proc report {where level} {
    # Where was the caller called from?
    set ns [uplevel 2 {namespace current}]
    yield "made $where $level context=$ns name=[info coroutine]"
}
proc example {} {
    report outer [info level]
}
namespace eval demo {
    proc example {} {
        report inner [info level]
    }
    proc makeExample {} {
        puts "making from [info level]"
        puts [coroutine coroEg example]
    }
    makeExample
}

And this is what it spits out:

making from 2
made inner 1 context=:: name=::demo::coroEg

Let's decode this, bestie, because it's giving ✨lore✨:

  • Compare the levels on the first line and the second line β€” that's stack manipulation happening, you can literally see it.
  • The parent level inside the coroutine is the global namespace (context=::).
  • Coroutine names are local to the current namespace unless you qualify them β€” hence ::demo::coroEg.
  • Coroutines can yield at depth, like from inside procedures that got called. So deep. πŸ’€

πŸ‘€ SEE ALSO πŸ‘€


πŸ”‘ KEYWORDS πŸ”‘

  • coroutine
  • generator

and that's the whole page, bestie. this was Tcl 8.6, page coroutine(n) β€” yes, the version and the date are chilling down there in the footer, right where they belong, and honestly? iconic of them. 🫢 go make something asynchronous and gorgeous. πŸ’–βœ¨πŸŽ€

πŸ“¦ where this page comes from