> You can think of algebraic effects essentially as exceptions that you can resume.
How is this substantively different than using an ApplicativeError or MonadError[0] type class?
> You can “throw” an effect by calling the function, and the function you’re in must declare it can use that effect similar to checked exceptions ...
This would be the declared error type in one of the above type classes along with its `raiseError` method.
> And you can “catch” effects with a handle expression (think of these as try/catch expressions)
That is literally what these type classes provide, with a "handle expression" using `handleError` or `handleErrorWith` (depending on need).
> Algebraic effects1 (a.k.a. effect handlers) are a very useful up-and-coming feature that I personally think will see a huge surge in popularity in the programming languages of tomorrow.
Not only will "algebraic effects" have popularity "in the programming languages of tomorrow", they actually enjoy popularity in programming languages today.
> How is this substantively different than using an ApplicativeError or MonadError[0] type class?
If you're limiting yourself to just a single effect there's probably not much difference, however once you have multiple effects at the same time then explicit support for them starts to become nicer than nesting monads (which requires picking an order and sometimes reorder them due to the output of some functions not matching the exact set or order of monads used by the calling function).
> nesting monads (which requires picking an order and sometimes reorder them due to the output of some functions not matching the exact set or order of monads used by the calling function).
mtl-style (which is where `MonadError` comes from in Haskell), is exactly to defer picking an order, and indeed a handler, until handling time. (I gather the GP was talking about something in Scala, but I guess it's the same.)
In Haskell, I see mtl and algebraic effects (say freer-simple) as giving you the same kind of expressiveness. The difference to me is that for mtl you need to figure out and abstract a new type class for every kind of effect and then write n^2 instances. While the freer monad construction needs only a single data type (often a GADT) and some glue function (calling send on constructors of said data type), and you are off to the races.
The algebraic reason for this is that effects are combined with sum, which is commutative up to isomorphism. While transformers are not naturally commutative, so mtl must write all the commuters as instances.
This, along with the reinterpret functions means that you can quickly spin up custom effects for your program, which do exactly what you need to express your program logic. Then all the glue coddle to make your program interact with the real world becomes a series of handlers, usually refining in several steps until you reach IO.
When I have used mtl, I end up only using the standard monad classes, and then I have to remember the semantics of each one in my domain.
Algebraic effects are in delimited continuation territory, operating on the program stack. No amount of monad shenanigans is going to allow you to immediately jump to an effect handler 5 levels up the call stack, update some local variables in that stack frame, and then jump back to execution at the same point 5 levels down.
Quite the opposite, that's exactly what continuation monads do, for example `ContT`, and more structured versions such as `freer`. Those essentially simulate a stack rather than using the actual RTS stack. For the latter there are `eff` and `bluefin-algae` (the latter very much work in progress). So yes, in Haskell at least, monads are the right API for deli meted continuations.
> Like goto, but you don't even need to name a label.
That's what exceptions are.
But effects don't cause you to see huge stack traces in errors because the whole point is that you provide the effect and values expected and the code goes on running.
>> Like goto, but you don't even need to name a label.
> That's what exceptions are.
In many contexts I agree. However, the difference here is exceptions are one-way execution control transfers.
Remember what was originally said:
No amount of monad shenanigans is going to allow you to
immediately jump to an effect handler 5 levels up the call
stack, update some local variables in that stack frame, and
then jump back to execution at the same point 5 levels down.
Jumping "5 levels up the call stack", modifying some state, then jumping back "5 levels down" is to me pretty much the definition of a nightmare to reason about.
> How is this substantively different than using an ApplicativeError or MonadError[0] type class?
It think it's about static vs. dynamic behavior.
In monadic programming you have to implement all the relevant methods in your monad, but with effects you can dynamically install effects handlers wherever you need to override whatever the currently in-effect handler would be.
I could see the combination of the two systems being useful. For example you could use a bespoke IO-compatible monad for testing and sandboxing, and still have effects handlers below which.. can still only invoke your IO-like monad.
They're pretty similar, but with different ergonomics. Algebraic effects are similar to some kind of "free" monad technique, but built in. For being built in they have nicer syntax and better composability, often. You can achieve the same in a language suitably dedicated to monadic approaches (Haskell being the poster child here) but it helps to have type class inference (giving you mtl-like composability) and built-in bind syntax a la Haskell's `do` or Scala's `for`.
Interesting. Thank you for your detailed explanation.
A related Scala-specific technique for this problem category is employing the Stackable Trait Pattern[0] to provide a functional style AOP[1] mechanism. Done carefully, effects (aspects) can be defined independent of specific logic as well as composed with provable invocation order.
Note that the references cited are for those reading this thread and not assumed to be required for the person addressed.
Sorry, Haskell’s “monad transformer library”. One of the earliest approaches to composability of multiple monadic effects. It’s pretty similar to an algebraic effect system allowing you to write effectual computations with types like `(Error m, Nondet m, WithState App m) => m ()` to indicate a computation that returns nothing but must be executed with access to error handling, nondeterminism, and access to the App type as state.
There are a few drawbacks to it, but it is a pretty simple way to get 80% of the ergonomics of algebraic effects (in Haskell).
For the MonadError in Haskell at least, it's quite similar. However, mtl-style has a number of issues that effect systems don't well explained by the author of effectful under "What about mtl?" at https://hackage.haskell.org/package/effectful.
> they actually enjoy popularity in programming languages today
They have enjoyed popularity amongst the Scala FP minority.
They are not broadly popular as they come with an unacceptable amount of downsides i.e. increased complexity, difficult to debug, harder to instantly reason about, uses far more resources etc. I have built many applications using them and the ROI simply isn't there.
It's why Odersky for example didn't just bundle it into the compiler and instead looked at how to achieve the same outcomes in a simpler and more direct way i.e. Gears, Capabilities.
If you want multishot continuations then I don't really know of any way other than delimited continuations (other than undelimeted continuations or simulating delimited continuations, on the heap).
Why can't the handler be invoked as if it was called from the effect invocation site, then return?
But apart from that and to answer your question there is an alternative to delimited continuations, and that's undelimited continuations (which essentially requires allocating call frames on the heap).
The handler doesn't have to follow the pattern of "do its work, resume the computation, go away."
It can instead do things like "do some work, resume the computation, do some more work."
Or even more invasively, "stash the computation somewhere, return from the handler site, let the rest of the program run for a while, then resume the computation."
How is this substantively different than using an ApplicativeError or MonadError[0] type class?
> You can “throw” an effect by calling the function, and the function you’re in must declare it can use that effect similar to checked exceptions ...
This would be the declared error type in one of the above type classes along with its `raiseError` method.
> And you can “catch” effects with a handle expression (think of these as try/catch expressions)
That is literally what these type classes provide, with a "handle expression" using `handleError` or `handleErrorWith` (depending on need).
> Algebraic effects1 (a.k.a. effect handlers) are a very useful up-and-coming feature that I personally think will see a huge surge in popularity in the programming languages of tomorrow.
Not only will "algebraic effects" have popularity "in the programming languages of tomorrow", they actually enjoy popularity in programming languages today.
https://typelevel.org/cats/typeclasses/applicativemonaderror...