[HN Gopher] Coroutines and effects
       ___________________________________________________________________
        
       Coroutines and effects
        
       Author : todsacerdoti
       Score  : 236 points
       Date   : 2024-04-20 14:39 UTC (2 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.
        
             | tialaramex wrote:
             | Right, this is a post which kinda assumes you _know_ Rust,
             | but AFAICT it isn 't a post specifically _about_ Rust.
             | 
             | In 2014 that would be extremely presumptuous or targeted at
             | a very niche audience, however in 2024 a _lot_ of people
             | know Rust and so it seems much more reasonable.
        
               | bitwize wrote:
               | Indeed. Rust is the language that the smart kids are
               | using to think about higher-order programming concepts.
               | It has begun to supplant Lisp and Haskell in that regard.
        
               | withoutboats3 wrote:
               | My reference point is Rust because I've worked on or in
               | Rust for most of the past decade, but I think the
               | relevance of Rust for programming language design more
               | broadly is that it is the most well-typed widely deployed
               | _imperative_ programming language. In this way it has an
               | advantage over Lisp or Haskell if you 're trying to think
               | about how we might statically analyze imperative
               | programs, which is what I'm personally most interested
               | in.
        
             | saghm wrote:
             | Interesting, I hadn't seen that context!
        
       | 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.
        
           | galaxyLogic wrote:
           | > An function with an effect (in this sense) is a function
           | which can ask a handler to `perform` some effect for it.
           | 
           | Why does it need to ask a "handler" to do something, why
           | can't it just call a function that does the "action" for it?
        
             | mrkeen wrote:
             | By making effects explicit, you reap the benefit of being
             | able to write non-effectful code.
             | 
             | Depending on what your language tracked as an effect, you
             | could make your business-logic always terminate, or perform
             | no allocations, if you had effects for
             | Mutation/GeneralRecursion/Allocation.
             | 
             | But no, I certainly don't understand the function->handler
             | control flow here. It has to be handler->function,
             | otherwise you've got two handlers!
        
             | reuben364 wrote:
             | a function will return to it's call site (or diverge), a
             | handler doesn't necessarily have to resume from where it
             | was invoked. There is also (sort of) dynamic scoping, where
             | you don't have to thread the handlers through calls.
        
         | 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.
        
               | naasking wrote:
               | Roughly, coroutine = a type of continuation with all
               | mutable state, and continuation = a type of immutable
               | coroutine.
        
               | dkjaudyeqooe wrote:
               | One lets you save and return to an execution state
               | (program counter and local environment), the other lets
               | you create and call an execution state that is saved
               | between calls to it.
               | 
               | There are obvious implementation differences but I'm not
               | sure it makes any difference here, in both cases you can
               | return to the same execution state multiple times.
        
               | Rusky wrote:
               | The difference is that resuming a coroutine mutates it,
               | so that the next time you resume the _same_ object it
               | starts from wherever the coroutine _next_ yielded. This
               | may or may not be the same yield point as the last time,
               | depending on the definition of the coroutine.
               | 
               | A continuation is immutable in that way, so it is either
               | an error to invoke it multiple times, or else it will
               | always resume at the same place. Implementing coroutines
               | in terms of continuations would mean capturing a new
               | continuation each time you yield.
        
               | samatman wrote:
               | The distinction between coroutines and delimited
               | continuations is one-shot vs. multi-shot. The delimited
               | continuation crowd use different language, but imagine an
               | ordinary stackful asymmetric coroutine wrapped around a
               | function call, except instead of just yield and resume,
               | you have yield, resume, and reset. Call the coroutine, it
               | yields from A, call resume, it yields from B, call reset,
               | resume, it yields from B again. You can do that as often
               | as you'd like.
               | 
               | This can in fact be emulated with a coroutine generator
               | and some fancy footwork, but it's a subtly different
               | primitive.
        
             | noelwelsh wrote:
             | That's my understanding, and why you need full
             | continuations to handle all effects. In my mental model a
             | coroutine gives you an execution point that you can resume,
             | but you are not allowed to resume execution points you have
             | previously resumed. You cannot, for example, implement
             | backtracking search with just coroutines as you need to to
             | return to previous execution points. (Look, you can
             | implement anything with anything. Turing completeness etc.
             | This is about implementing it in a natural way using the
             | effect handlers.)
        
         | naasking wrote:
         | > Feels like comparing Lists with functions, or promises with
         | interfaces
         | 
         | Functions and lists are technically isomorphic, you just
         | replace the function with a list of (domain, codomain) pairs
         | and function invocation then becomes list lookup. This is
         | basically the set theory definition of a function. So yes, this
         | comparison to the article is apt, the article is saying that
         | you can encode effects via coroutines.
        
         | mamcx wrote:
         | This is like monads, is better to not look at the definition
         | and just get examples.
         | 
         | I like the way is presented here:
         | https://mikeinnes.io/posts/transducers/
         | 
         | The main gist is that "effect" allows you to define your own
         | "except" of "try/except/finally"
        
           | mrkeen wrote:
           | I make use of monads as effects, and that's why I'm not
           | getting this article.
           | 
           | I wrote "Feels like comparing Lists with functions" for a
           | more general audience, but in my mind I was thinking:
           | - Effects are Monads       - Continuations are one particular
           | Monad [1]       - Coroutines are probably similar?       - I
           | would use monadic effects to allow/disallow a function from
           | making use of Coroutines.       - If my effect system
           | *itself* uses Coroutines how do I use the effect system to
           | forbid Coroutines?
           | 
           | [1] https://hackage.haskell.org/package/mtl-2.3.1/docs/Contro
           | l-M...
        
       | 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
        
         | ordinaryradical wrote:
         | Do itttttt :)
        
         | wahern wrote:
         | See also aspect-oriented programming (AoP):
         | https://en.wikipedia.org/wiki/Aspect-oriented_programming
         | Algebraic effects might be a little too strict and narrowly
         | defined for something as generic as, e.g., injecting ad hoc
         | logging. With algebraic effects you need to reify in the type
         | system the specific control flow behaviors you care about, and
         | then get code to use those types and operators accordingly. AoP
         | seems like a higher-level (if looser) concept that isn't
         | necessarily specifically bound to the particular details of the
         | type system. That modeling freedom seems necessary unless
         | you're creating a language from scratch or confining yourself
         | to a very limited set of control effects that can be shoe-
         | horned into the existing type system and control flow
         | operators.
        
         | valcron1000 wrote:
         | You already have something like this (production ready) through
         | effect systems in Haskell (ex.
         | https://hackage.haskell.org/package/effectful)
        
         | bandrami wrote:
         | Like clockwork, every few years, smart people will re-invent
         | introspection.
        
       | 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
        
             | naasking wrote:
             | Unless it can't allocate, and then the whole program
             | terminates. There are unfortunately limitations everywhere.
        
               | metadat wrote:
               | Is it really worth worrying about? This strikes me as
               | pedantic. Once a program can no longer allocate memory,
               | it's very likely toast anyway.
               | 
               | Edit: @naasking, all good points. Still, isn't the
               | general strategy to ensure memory isn't exhausted, rather
               | than handling such a situation "gracefully", whatever
               | that means? :)
        
               | naasking wrote:
               | It depends what your program is for. Some programs need
               | guarantees because of bounded resources, so you may want
               | resource allocation to be a visible effect (like embedded
               | systems), or because you're running potentially untrusted
               | code (like JS or WASM in a browser), or because you want
               | sane failure handling (like running modules in a web
               | server).
        
               | arzig wrote:
               | Graceful is a misnomer, but there are better versions. If
               | you are day, a database server, and you run out of memory
               | and die horribly, availability for all clients is
               | compromised. If you begin rejecting queries or
               | connections until memory js available at least some
               | availability is maintained.
        
               | tialaramex wrote:
               | Of course as well as limiting the total amount of memory
               | a running program is allowed to allocate, we can also
               | limit how many CPU instructions it can execute and
               | numerous other (OS dependent) resource uses. Accounting
               | for this all operations are fallible, which no practical
               | language copes with.
               | 
               | It's very normal in a Unix to be allowed to limit total
               | runtime for example.
        
               | Kinrany wrote:
               | If the whole program terminates, the purity of the
               | function is still intact, the computer just can't compute
               | it :D
        
           | amluto wrote:
           | It's certainly pure in the Haskell sense: the inputs are two
           | immutable values, and the output is another immutable value.
           | It's even pure in some Rusty senses: it could take String or
           | &str as input and returns a String -- in a model where String
           | is a value, it's pure.
           | 
           | A lot of modern programming logically involves telling a
           | computer to what to compute and not particularly caring _how_
           | it gets computed. Yet we mostly lack a language in which one
           | can specify computations and then then run the result
           | portably and safely.
        
         | kuchenbecker wrote:
         | Hack programming language (Facebook's php++). They are called
         | coeffects and can range from "this is pure with no side effect"
         | to "this can modify local members" to "this has I/O".
        
         | vendiddy wrote:
         | I would love something like this where to write a function with
         | side effects I'd have to annotate it as such.
         | 
         | Then the compiler could recursively understand whether any
         | given function is pure.
        
           | cryptoxchange wrote:
           | The solidity language for smart contracts works like this.
        
           | mrkeen wrote:
           | I write code like this every day in Haskell.
        
         | dustingetz wrote:
         | don't forget unbounded loops diverge - while(true){} and fn f()
         | {f()} - also impure are blocking "pure" computations like
         | fib(50)
        
           | skybrian wrote:
           | In proof languages where you want a proof that a function
           | will succeed _without running it,_ knowing that it doesn 't
           | diverge is important.
           | 
           | But if you're going to do the calculation, there's no
           | difference between an infinite loop and a loop that finishes,
           | but will take longer than you're willing to wait. Either way,
           | it looks like the program hung.
           | 
           | So in practical programming languages, I'm not sure that the
           | "diverge" effect is useful? You still need to kill it if it
           | hangs. If it will take a long time, returning some kind of
           | progress indicator would be nice. Maybe writing it as an
           | iterator would help with that?
        
             | codebje wrote:
             | It may depend on what you mean by 'practical', perhaps, but
             | in a Haskell-like language divergence (as opposed to
             | induction) more or less takes the form "let x = x in x"
             | with varying degrees of ceremony. If you can provide to the
             | compiler that you don't have any such thing in your
             | fragment of code it can omit black-hole updates and checks
             | during garbage collection for a small (possibly negligible
             | or nil, if divergence still exists in the language anyway)
             | performance boost.
             | 
             | As you suggest, explicitly adding a provably-reducing value
             | like a maximum iteration limit lets you show progress, give
             | up at a "reasonable" time, and avoid divergence. This is
             | how unlifted programming languages permit general recursion
             | - that, and allowing inductive types to merely prove
             | they're productive within a finite time bound (eg, a stream
             | of values only needs to show it produces the next value
             | eventually, not that it produces all values eventually).
        
         | brabel wrote:
         | D has a lot of these kinds of "effects", though they're called
         | "attributes" [1] like "nothrow", "pure", "safe", "mustuse",
         | "nogc"... there's also the "noreturn" type for functions that
         | will never return [2] (infinite loop or throws).
         | 
         | [1] https://dlang.org/spec/attribute.html#return
         | 
         | [2] https://dlang.org/spec/type.html#noreturn
        
       | Avi-D-coder wrote:
       | > understand when the effect occurs requires examining the type
       | signature of every function that is called. Since this is
       | meaningful control flow, it seems very valuable to be able to
       | identify points at which an error occurs without examining the
       | signatures of each function call.
       | 
       | This is currently the case in rust. IO and other effects are
       | frequently implicit. You don't have to use ? or await they are
       | *sugar. I have frequently seen reinventions of exceptions, unwind
       | nonsense, adhoc interpreted tagged effects, etc..
       | 
       | Explicit syntax for effectfull calls should not be a goal. We
       | don't actually have that today.
        
         | skavi wrote:
         | I think it's incredibly useful today to have both of these
         | annotations.
         | 
         | I frequently scan for all instances of `?` or `.await` in
         | functions (though, unfortunately, for various reasons this
         | won't show you everywhere these effects are produced).
         | 
         | I would rather not have to rely on an IDE to get that
         | functionality.
        
       | jeffparsons wrote:
       | > The key difference between coroutines and effect handlers is
       | that coroutines yield control to their caller, but effectful
       | expressions yield control to their handler. The difference of
       | affordance this implies is the materially significant advantage
       | of coroutines over effect handlers.
       | 
       | Should that last bit be: "advantage of effect handlers over
       | coroutines"?
       | 
       | EDIT: Oh, maybe I should have read the rest of the article first.
       | It might be going the other way than that one example suggested.
       | :)
        
       | kodablah wrote:
       | > Overall, coroutines strike me as the most promising way to
       | handle many kinds of effectful functions
       | 
       | This is basically what we do at Temporal, which is essentially
       | deterministic coroutines with externalized effects. This gives
       | another nice property: using coroutines to wrap effects lets the
       | non-effect logic replay/resume durably.
        
       | orthoxerox wrote:
       | > as far as I understand it is not possible to meaningfully
       | handle the diverging effect
       | 
       | I don't know if that counts, but I think
       | `call_with_timeout(duration, function_that_diverges,
       | timeout_return_value, args...)` handles the diverging effect of
       | its function argument.
        
         | withoutboats3 wrote:
         | The difference of this and an effect handler fwiw is that it
         | doesn't handle it _totally_ - it handles the divergence effect
         | but then produces a partial function.
        
           | orthoxerox wrote:
           | Why does it produce a partial function? If the argument is
           | total other than divergence, call_with_timeout fills in all
           | "holes" in the set of all possible returns with default
           | values.
        
             | withoutboats3 wrote:
             | I'm assuming your language has types which don't have
             | default values because default values for every type are a
             | billion dollar mistake that no modern language should have.
        
               | mitt_romney_12 wrote:
               | I'm assuming `timeout_return_value` would be a user
               | provided value that serves as the default. But most
               | effect systems also support a `return` effect that lets
               | change the return type of a function [1]. So you could
               | make it return `Just<result>` when it succeeds or
               | `Nothing` when it hits the timeout.
               | 
               | [1] https://koka-lang.github.io/koka/doc/book.html#sec-
               | return
        
       | fire_lake wrote:
       | I'm personally betting big on OCaml due to the effect system
       | work. I think this is one of the next big advances in industrial
       | programming.
       | 
       | - Lambdas (mainstream)
       | 
       | - Static types (mainstream)
       | 
       | - Pattern matching (getting there)
       | 
       | - Sum types (getting there)
       | 
       | - TCO (getting there)
       | 
       | - Global type inference (future)
       | 
       | - Functors (future)
       | 
       | - Effect systems (future)
       | 
       | - Expression orientated (future)
       | 
       | With OCaml, I get all of this today.
       | 
       | The future is here, it's just not evenly distributed...
        
         | dureuill wrote:
         | Global type inference is not a positive in my book. In my
         | experience it becomes very hard to understand the types
         | involved if they are not explicit at systems boundaries.
         | 
         | I can also imagine that it must be hard to maintain, like
         | sometimes the types must accidentally change
        
           | fire_lake wrote:
           | There's a trade off - was the mistake here or there? The type
           | checker cannot know. But for those few cases you can add an
           | annotation. Then the situation is, in the worst case, as good
           | as when types are mandatory.
        
             | dureuill wrote:
             | > But for those few cases you can add an annotation.
             | 
             | not in other people's code. My main concern is that gradual
             | typing makes understanding other people's code more
             | difficult.
             | 
             | Idiomatic Haskell warns against missing signatures[1], Rust
             | makes them mandatory. Rather than global inference, local
             | inference stopping at function boundaries is the future, if
             | you ask me.
             | 
             | [1]: https://wiki.haskell.org/Type_signatures_as_good_style
        
             | mitt_romney_12 wrote:
             | > the situation is, in the worst case, as good as when
             | types are mandatory
             | 
             | The worst case is actually worse than when types are
             | mandatory, since you can get an error in the wrong place.
             | For example, if a function has the wrong type inferred then
             | you get an error when you use it even though the actual
             | location of the error is at the declaration site. Type
             | inference is good but there should be some places (ex.
             | function declarations) where annotations are required.
        
           | grumpyprole wrote:
           | Being hard to maintain and having no static types at all, did
           | not stop Python rising to conqueror the world. Type inference
           | allows us to at least give those users the succinctness they
           | are used to, if not the semantics. Those who like explicit
           | types can add as many annotations as they need in OCaml.
        
             | dureuill wrote:
             | > Those who like explicit types can add as many annotations
             | as they need in OCaml.
             | 
             | They cannot add it in other people's libraries.
             | 
             | > did not stop Python rising to conqueror the world
             | 
             | I wasn't talking popularity, I was talking maintainability.
             | Python is not a stellar example of maintainability (source:
             | maintained the Python API for a timeless debugger for 5
             | years).
             | 
             | Python's ubiquity is unfortunate, thankfully there seems to
             | be a movement away from typeless signatures, both with
             | Python's gradual typing (an underwhelming implementation of
             | gradual typing, unfortunately) and Typescript.
        
               | grumpyprole wrote:
               | > They cannot add it in other people's libraries.
               | 
               | Does it matter that much how the internals of someone
               | else's library are implemented? The tooling will tell you
               | the types anyway and module interfaces will have type
               | signatures.
               | 
               | > Python's ubiquity is unfortunate,
               | 
               | Well that we can agree on!
        
         | naasking wrote:
         | The lack of function overloading was really the only thing
         | holding OCaml back 15 years ago. Last I checked, there was no
         | interest in changing this.
        
           | sapiogram wrote:
           | That can't really be the main reason, Rust is doing just fine
           | without it.
        
             | naasking wrote:
             | Rust has overloading via traits.
        
       | grumpyprole wrote:
       | > but he made me realize that effect systems (like that found in
       | Koka) and coroutines (like Rust's async functions or generators)
       | are in some ways isomorphic to one another.
       | 
       | It's a shame this was not explored before going down the Async
       | route. I believe Effect handlers would have been a better fit for
       | a systems language. IMHO a Monadic approach really needs
       | something expressive like a functional language in order to work
       | well.
        
       | gpderetta wrote:
       | > what if it was dynamically scoped.... but statically
       | typed..............?
       | 
       | That's the right insight. Just give me implicit parameters (i.e.
       | the statically typed versions of dynamic scoping). I'll build my
       | own effect system, coroutines, exception and what not as needed.
        
         | solidninja wrote:
         | Indeed - I do not think it is a coincidence that a lot of
         | production experiments in effect systems are happening in Scala
         | right now - the language is very flexible to conduct them.
         | https://github.com/getkyo/kyo in particular looks interesting
         | as it explores a different space where the monadic nature is
         | less exposed to the end user.
        
       | ufo wrote:
       | This classification seems somewhat Rust-centrict. The terminology
       | I'm more used to seeing for coroutines is this:
       | 
       | Symmetric vs assymetric coroutines: symmetric coroutines are the
       | original style, where the currently executing coroutine decides
       | who to transfer control to. Assymetric coroutines have a caller
       | and a callee and the callee always yields control back to the
       | caller. (This is much more common today)
       | 
       | Stackful vs stackless coroutines: in a stackless setting the
       | language distinguishes between synchronous and asynchronous
       | functions. This is what Python and Rust generators do. In a
       | stackless setting (such as Lua), every function can potentially
       | yield or call a sub-function that yields.
       | 
       | Exception handling is somewhat similar to stackless coroutines,
       | specially in languages such as Common Lisp where the exception
       | handler can choose to resume execution from the point that raised
       | the exception, instead of unwinding the stack.
        
         | samatman wrote:
         | > _symmetric coroutines are the original style_
         | 
         | This isn't quite correct, but it's close. The original style
         | were co-routines, as distinguished from subroutines, this was
         | when programming was still heavily goto-oriented, these were
         | little more than patterns used to structure go-to.
         | 
         | Simula, if I recall correctly, was the first high-level
         | language to have coroutines, and they were basically
         | asymmetric. I say basically because there was a mechanism for a
         | coroutine to replace itself with another coroutine, but it
         | wasn't a general transfer mechanism. Not unlike a delimited
         | continuation in fact.
         | 
         | Good summary of how it all works however! I believe this is a
         | typo:
         | 
         | > _In a stackless setting (such as Lua)_
         | 
         | Since Lua is stackful, and you're contrasting it with stackless
         | generators.
        
           | ufo wrote:
           | yup, was a typo >_<
        
         | gpderetta wrote:
         | I don't see how exception handling is similar to stackless
         | coroutine as it is very non local.
        
           | ufo wrote:
           | Sorry, I meant stackful (was a typo)
        
       ___________________________________________________________________
       (page generated 2024-04-22 23:02 UTC)