[HN Gopher] Coroutines and effects
___________________________________________________________________
Coroutines and effects
Author : todsacerdoti
Score : 89 points
Date : 2024-04-20 14:39 UTC (1 days ago)
(HTM) web link (without.boats)
(TXT) w3m dump (without.boats)
| nsm wrote:
| I am not an expert on this either, although very interested. Note
| that delimited continuations, which are a superset of coroutines
| are identical to effects in the sense of yielding control to the
| handler, not the caller. In languages like Racket/Scheme which
| allows associating tags with handlers, you can also model the
| same function having multiple effects.
|
| In general, coroutines seem vastly easier to understand/type
| compared to effects unless your language can do a lot of effect
| inference.
| angiosperm wrote:
| After scrutiny this seems to be "Coroutines and Effects" in,
| particularly, Rust.
| saghm wrote:
| Given that withoutboats is a long-time major contributor to
| Rust, I suspect they titled the blog post with the audience of
| regular readers of their blog in mind rather than a more
| general audience like Hacker News
| steveklabnik wrote:
| It's more than that:
| https://bsky.app/profile/without.boats/post/3kql3yr3goc23
|
| > Btw this is the beginning of me trying to shift away from
| blogging about rust to blogging about PL design in general. I
| find that I have very little to say about Rust that I haven't
| already said.
| armchairhacker wrote:
| What's the difference between an effect and a "coroutine" where
| calls in the function body to other coroutines are implicitly
| yielded? Or is that not a real coroutine?
|
| That's what Kotlin's `suspend fun` does.
| Rusky wrote:
| Both effects and coroutines can (and typically do) work
| implicitly across calls like that. The version with forwarding
| operators like in this post is something of a Rust-ism, or more
| generally an async/await-ism, or a stackless coroutine-ism.
|
| The more fundamental difference between effects and coroutines
| is that a `yield` in a coroutine always goes to the one unique
| resumer, and carries a single type of value. On the other hand,
| an expression may have several effects, each of which may be
| handled in a different place, in the same way that distinct
| exception types may be caught in different places.
| rc_mob wrote:
| I just upvoted this because I love the domain name.
| mrkeen wrote:
| I'm not really understanding the comparison (or isomorphism)
| between coroutines and effects. Feels like comparing Lists with
| functions, or promises with interfaces.
| alephaleph wrote:
| A coroutine is a computation that can `yield`, suspending
| itself and passing control back up to the caller. It can then
| be resumed at the caller's leisure.
|
| An function with an effect (in this sense) is a function which
| can ask a handler to `perform` some effect for it. This
| suspends the function and passes control to whichever handler
| is in scope for that call, allowing that handler to resume the
| function at its leisure.
|
| I suspect that you're misunderstanding what is meant by effect,
| because despite buzz about them and backend support for them in
| OCaml 5, they aren't yet implemented with syntax and type-level
| support in any mainstream languages I'm aware of.
| noelwelsh wrote:
| I don't know your background, so I don't know at what level to
| pitch an explanation. Here's an attempt that assumes some
| knowledge of modern PLs.
|
| Effects require 1) well-defined control flow (think of IO; you
| need to know in what order output occurs) and 2) manipulation
| of control flow (think of error handling or concurrency).
|
| We can model effects as a back-and-forth between effect
| handlers, which carry out effects, and the user program. The
| user program passes control to the effect handler to carry out
| some effect, and the effect handler passes control back to the
| user program (potentially a different part of the user program;
| think error handling) when the effect has been performed.
| Continuations give complete control over control flow, so in
| their full generality effects require continuations (or some
| equivalent like monads). Coroutines are a slightly stilted form
| of continuations, that you can model much, but not all, control
| flow with.
| zozbot234 wrote:
| Aren't coroutines generally one-shot, whereas continuations
| could potentially be resumed to multiple times? This seems to
| be a relevant difference between these concepts.
| dkjaudyeqooe wrote:
| No, the whole point of coroutines is that you can resume
| them multiple times, otherwise it's just a simple function
| call.
| Rusky wrote:
| That's not what "resume multiple times" is referring to
| here. You can typically only resume a coroutine once _per
| yield,_ while a continuation generally allows you to
| return to the _same place_ multiple times.
| et1337 wrote:
| Wow I've been thinking of something exactly like this! Sort of a
| super charged, statically typed version of Go's context.Context.
| It would allow you to describe the capabilities (or effects) of
| every function, including IO, memory allocation, cancellation,
| deadlines, concurrency, and whatever application-specific stuff
| you need on there (like logging).
|
| Then you could implement something like Google capslock [0] just
| by looking at type signatures.
|
| 0. https://github.com/google/capslock
| amluto wrote:
| This touches on something I'd like to see in more mainstream
| languages, but it doesn't quite get there.
|
| > For example, Koka has a "diverging" effect, which means that an
| expression may diverge (that is to say, it may not finish
| evaluating). An expression containing a diverging expression is
| also diverging. So you can distinguish in the type system between
| a function that is guaranteed to finish and a function that may
| not finish (this is imperfect, of course, because of the
| undecidability of the halting problem; some functions that do not
| diverge will be marked diverging).
|
| As I think about it (and I'm not a programming language theorist,
| nor have I done much serious work in any language with any sort
| of effect system), there are two vague categories of effect:
| control-flow effects like exceptions, yields, async waits
| (Pending, sleep, or however you feel like modeling it) and non-
| control-flow effects (divergence, various forms of unsafety,
| nondeterminism, impurity, reads or writes of global state,
| syscalls in models that don't treat IO in and of itself as an
| effect), etc.
|
| I would like to be able to run and write code that is
| _definitely_ free of certain kinds of effects. Xz should not be
| unsafe or do IO, for example. Leftpad is an entirely pure, non-
| diverging function. And I should be able to ask my language to
| enforce that, ideally with trivial code. Maybe even by default.
|
| But mainstream languages seem to mostly limit their use of
| effect-like systems on the control flow part, like this:
|
| > Overall, coroutines strike me as the most promising way to
| handle many kinds of effectful functions because they seem to be
| in the design sweet spot: They are statically typed, lexically
| scoped, and unlayered.
| noelwelsh wrote:
| This is one of the motivations of algebraic effects: you can
| have fine grained control over which effects are allowed.
| Rusky wrote:
| There are two aspects to effects as a language feature- first
| there is the runtime behavior, and then there is the type
| system. Ruling out categories of effects is the job of the type
| system, and you don't have to commit to (or avoid) any
| particular runtime approach to use it that way.
|
| This is related to the vagueness of your two categories. While
| exceptions, yields, and awaits don't really make sense without
| a handler, all that matters to the type system is which
| operations an expression might perform _in addition to_
| producing a result of their primary type. Handlers only
| interact with the type system in the sense that they remove an
| effect from their handle-ee expression.
|
| So even in a language that committed to coroutines as its
| approach handling effects at runtime, the type system could
| still track and rule out effects like divergence or system
| calls. (For what it's worth, nondeterminism, state, etc. can
| all also be defined in terms of handlers. And at the same time,
| though, unsafety is not an effect because it is not entirely
| captured by "can this expression perform operation X.")
| newpavlov wrote:
| Yeah, the whole blog post reads like "if you worked too much
| with a hammer, everything starts looking like a nail". In other
| words, it puts the cart before the horse.
|
| Effects is a MUCH more general and powerful technique than
| coroutines. An effect system can help greatly with implementing
| a coroutine system (instead of dealing with the unfortunate
| type-level hack Rust uses), but trying to implement an effect
| system on top of a coroutine system will give you a very
| limited emulation in the best case and another incoherent mess
| in the worst.
|
| It would've been really great if Rust had a proper effect
| system, but it's very hard to introduce it into an existing
| language similarly to how you can not easily introduce borrow
| checker to C/C++. The messy "keyword generics" proposals only
| serve as a good demonstration for this. So we probably have to
| wait for Rust 2 or a different successor language.
| klabb3 wrote:
| > Leftpad is an entirely pure, non-diverging function.
|
| `leftpad(string, int) -> string` needs to allocate so not fully
| pure that way. You could pass in the resulting string but then
| it would have to mutate, which is not really pure either.
| atlintots wrote:
| It would be pure in languages without manual memory
| management
___________________________________________________________________
(page generated 2024-04-21 23:00 UTC)