← Back to context

Comment by tome

10 hours ago

I was completely baffled by "algebraic effects" for years. They looked far too confusing for me to want to spend my time on them, and took the "Don’t feel like you have to [get curious about them]" approach.

But then at some point it struck me: underlying all these effect systems is just passing stuff in. So I developed my own effect system for Haskell, Bluefin[1], based on capabilities, which means the "capability to perform some effect" is represented by just passing stuff in (that is, a function can do some effect as long as it has been passed the capability to do it).

From this point of view it's hard to understand the excitement over "resume with" and "the part you can’t do with try / catch. It lets us jump back to where we performed the effect, and pass something back to it from the handler". Programming languages have had that feature since forever: a "resumable exception" is a "function call". A dynamically chosen "resumable exception" is the call of a dynamically chosen function, i.e. the argument to a higher order function.

So I don't know why people love the complexity around "algebraic effects". Maybe the mystique has a certain allure. But if you want the most straightforward possible approach I can recommend you try out Bluefin. I'm happy to answer questions on the issue tracker[2].

(Caveat: Bluefin is able to simplify things dramatically by dropping support for "multi-shot" continuations. But mostly you don't want multi-shot continuations.)

EDIT: I was too pessimistic, bazoom42 has noticed this :) https://github.com/tomjaguarpaw/bluefin/issues/new

A real effect system allows you to do things like NOT continue execution after using the effect (like the error effect does - if you "implement" this by using Exceptions, you're not using effects at all, just using Exceptions with extra steps) or only continuing it after some asynchronous work happens (the Future effect), or even "continue" execution several times. That just cannot be done with "just passing stuff in". You still don't seem to have understood effects.

  • Thanks for your response. Perhaps I'm missing some fundamental things. Could you help?

    > A real effect system allows you to do things like NOT continue execution after using the effect

    Right, Bluefin's Request allows you to do that too. For example here is an example of handling the request by continuing or not, depending on what the value yielded to the Request is.

        example :: Either String ()
        example = runPureEff $ try $ \ex -> do
          forEach
            ( \r -> do
                request r True
                request r True
                request r False
                request r True
                request r True
            )
            ( \case
                True -> pure ()
                False -> throw ex "Stopped"
            )
    

    > if you "implement" this by using Exceptions, you're not using effects at all, just using Exceptions with extra steps

    Not sure I follow that. Above you can see I used an exception (Bluefin's Throw capability), but I couldn't have used only an exception because that would have aborted unconditionally. What am I missing here, that makes "using Exceptions" "not using effects at all"?

    > only continuing it after some asynchronous work happens (the Future effect)

    I'm not really sure what "a Future effect" is, but I don't see how it's not something that can be run as a function call, at least in Haskell.

    > or even "continue" execution several times

    Right, these are the multishot continuations which Bluefin doesn't support. I haven't discovered many particularly compelling use cases for multishot continuations but would be very interested in finding some. The developer of the Kyo effect system for Scala, Flavio Brasil, suggested parsing, with multiple parse results, which makes sense.

    I'm also not entirely sure Bluefin couldn't simulate common use cases of multishot continuations with threads, but I haven't thought about it very hard.

    > You still don't seem to have understood effects.

    Possibly true, and part of my puzzlement! I'm always happy to try to improve my understanding. Can you help me see what I've missed?

Equating "algebraic effects" with "continuations" is like saying "if" is just "goto" (which isn't even true, e.g., an if can turn into a cmov or whatever).

The only mystique around algebraic effects is the same mystique there is around monads. I don't know if people have started equating algebraic effects to burritos yet but that's a pretty good way to take something simple and turn it into something confusing.

  • > Equating "algebraic effects" with "continuations" is like saying "if" is just "goto"

    Fair enough. But are you responding to something I said? I didn't make that equation.

    > The only mystique around algebraic effects is the same mystique there is around monads. I don't know if people have started equating algebraic effects to burritos yet but that's a pretty good way to take something simple and turn it into something confusing.

    Ah, are you saying that fundamentally there isn't really much to algebraic effects and they're much simpler than they're made out to be? If so then it perhaps we agree?

Continuations (“function calls”) reduce the amount of optimisations you can do around memory - both space wise and computationally