my_function (): Unit can AllErrors =
x = LibraryA.foo ()
y = LibraryB.bar ()
The first thing to note is that there is no indication that foo or bar can fail. You have to lookup their type signature (or at least hover over them in your IDE) to discover that these calls might invoke an error handler.
The second thing to note is that, once you ascertain that foo and bar can fail, how do you find the code that will run when they do fail? You would have to traverse the callstack upwards until you find a 'with' expression, then descend into the handler. And this cannot be done statically (i.e. your IDE can't jump to the definition), because my_function might be called from any number of places, each with a different handler.
I do think this is a really neat concept, but I have major reservations about the readability/debuggability of the resulting code.
> [...] how do you find the code that will run when they do fail? You would have to traverse [...]
I work in a .NET world and there many developers have this bad habit of "interface everything", even if it has just 1 concrete implementation; some even do it for DTOs. "Go to implementation" of a method, and you end up in the interface's declaration so you have to jump through additional hoops to get to it. And you're out of luck when the implementation is in another assembly. The IDE _could_ decompile it if it were a direct reference, but it can't find it for you. When you're out of luck, you have to debug and step into it.
But this brings me to dependency injection containers. More powerful ones (e.g., Autofac) can establish hierarchical scopes, where new scopes can (re)define registrations; similar to LISP's dynamically scoped variables. What a service resolves to at run-time depends on the current DI scope hierarchy.
Which brings me to the point: I've realized that effects can be simulated to some degree by injecting an instance of `ISomeEffectHandler` into a class/method and invoking methods on it to cause the effect. How the effect is handled is determined by the current DI registration of `ISomeEffectHandler`, which can be varied dynamically throughout the program.
(Alternately, inject it as a class member.) Now, the currently installed implementation of `IErrorConditions` can throw, log, or whatever. I haven't fully pursued this line of though with stuff like `yield`.
abathologist · 1h ago
> The first thing to note is that there is no indication that foo or bar can fail
I think this is a part of the point: we are able to simply write direct style, and not worry at all about the effectual context.
> how do you find the code that will run when they do fail
AFAIU, this is also the point: you are able to abstract away from any particular implementation of how the effects are handled. The code that will when they fail is determined later, whenever you decide how you want to run it. Just as, in `f : g:(A -> B) -> t(A) -> B` there is no way to find "the" code that will run when `g` is executed, because we are abstracting over any particular implementation of `g`.
nine_k · 50m ago
It looks like exceptions (write the happy path in direct style, etc), but with exceptions, there is a `catch`. You can look for it and see the alternate path.
What might be a good way to find / navigate to the effectual context quickly? Should we just expect an IDE / LSP color it differently, or something?
MrJohz · 34m ago
There's a `catch` with effects as well, though, the effect handler. And it works very similarly to `catch` in that it's not local to the function, but happens somewhere in the calling code. So if you're looking at a function and you want to know how that function's exceptions get handled, you need to look at the calling code.
No comments yet
MrJohz · 1h ago
> And this cannot be done statically (i.e. your IDE can't jump to the definition), because my_function might be called from any number of places, each with a different handler.
I believe this can be done statically (that's one of the key points of algebraic effects). It works work essentially the same as "jump to caller", where your ide would give you a selection of options, and you can find which caller/handler is the one you're interested in.
wavemode · 26m ago
> there is no indication that foo or bar can fail
Sounds like you're just criticizing try-catch style error handling, rather than criticizing algebraic effects specifically.
Which, I mean, is perfectly fair to not like this sort of error handling (lack of callsite indication that an exception can be raised). But it's not really a step backward from a vast majority of programming languages. And there are some definite upsides to it as well.
AdieuToLogic · 2h ago
> 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?
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.
threeseed · 6m ago
> 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.
davery22 · 1h ago
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.
grg0 · 1h ago
That sounds like a fucking nightmare to debug. Like goto, but you don't even need to name a label.
vkazanov · 14m ago
Well, you test the fact that the handler receives the right kind of data, and then how it processes it.
And it is useful to be able to provide these handlers in tests.
Effects are AMAZING
anon-3988 · 2h ago
I don't really get it, but is this related to delimited continuation as well?
As I understand it this was the inspiration for React's hooks model. The compiler won't give you the same assurances but in practice hooks do at least allow to inject effects into components.
YuukiRey · 10m ago
I don’t see the similarity. Since hooks aren’t actually passed to, or injected into components, there’s no way to evaluate the same hooks in different ways.
I can’t have a hook that talks to a real API in one environment but to a fake one in another. I’d have to use Jest style mocking, which is more like monkey patching.
From the point of view of a React end user, there’s also no list of effects that I can access. I can’t see which effects or hooks a component carries around, which ones weren’t yet evaluated, and so on.
ww520 · 9m ago
It's different. Hooks in React is basically callback on dependency of state changes. It's more similar to the signaling system.
rixed · 35m ago
Maybe I'm too archaic but I do not share the author's hope that algebraic effects will ever become prevalently used. They certainly can be useful now and then, but the similitude with dynamic scoping brings too many painful memories.
cdaringe · 3h ago
I did protohackers in ocaml 5 alpha a couple of years ago with effects. It was fun, but the toolchain was a lil clunky back then. This looks and feels very similar. Looking forward to seeing it progressing.
abathologist · 1h ago
Effects in OCaml 5.3 are quite a bit cleaner than there were a few years back (tho still not typed).
wild_egg · 2h ago
> You can think of algebraic effects essentially as exceptions that you can resume.
So conditions in Common Lisp? I do love the endless cycle of renaming old ideas
valcron1000 · 28m ago
No, algebraic effects are a generalization that support more cases than LISP's condition system since continuations are multi-shot. The closest thing is `call/cc` from Scheme.
Sometimes making these parallelism hurts more than not having them in the first place
riffraff · 1h ago
Also literal "resumable exceptions" in Smalltalk.
knuckleheads · 43m ago
First time in a long while where I’ve read the intro to a piece about new programming languages and not recognized any of the examples given at all even vaguely. How times change!
charcircuit · 3h ago
This doesn't give a focused explaination on why. I don't see how dependency injection is a benefit when languages without algebraic effects also have dependency injection. It doesn't explain if this dependency injections is faster to execute or compile or what.
yen223 · 2h ago
The way dependency injection is implemented in mainstream languages usually involves using metaprogramming to work around the language, not with the language. It's not uncommon to get errors in dependency-injected code that would be impossible to get with normal code.
It's interesting to see how things can work if the language itself was designed to support dependency injection from the get-go. Algebraic effects is one of the ways to achieve that.
vlovich123 · 2h ago
Don’t algebraic effects offer a compelling answer to the color problem and all sorts of related similar things?
charcircuit · 1h ago
>It's interesting
Which is why I was asking for that interesting thing to be written in the article on why it would better.
The second thing to note is that, once you ascertain that foo and bar can fail, how do you find the code that will run when they do fail? You would have to traverse the callstack upwards until you find a 'with' expression, then descend into the handler. And this cannot be done statically (i.e. your IDE can't jump to the definition), because my_function might be called from any number of places, each with a different handler.
I do think this is a really neat concept, but I have major reservations about the readability/debuggability of the resulting code.
All of what you state is very doable.
I work in a .NET world and there many developers have this bad habit of "interface everything", even if it has just 1 concrete implementation; some even do it for DTOs. "Go to implementation" of a method, and you end up in the interface's declaration so you have to jump through additional hoops to get to it. And you're out of luck when the implementation is in another assembly. The IDE _could_ decompile it if it were a direct reference, but it can't find it for you. When you're out of luck, you have to debug and step into it.
But this brings me to dependency injection containers. More powerful ones (e.g., Autofac) can establish hierarchical scopes, where new scopes can (re)define registrations; similar to LISP's dynamically scoped variables. What a service resolves to at run-time depends on the current DI scope hierarchy.
Which brings me to the point: I've realized that effects can be simulated to some degree by injecting an instance of `ISomeEffectHandler` into a class/method and invoking methods on it to cause the effect. How the effect is handled is determined by the current DI registration of `ISomeEffectHandler`, which can be varied dynamically throughout the program.
So instead of writing
you establish an error protocol through interface `IErrorConditions` and write (Alternately, inject it as a class member.) Now, the currently installed implementation of `IErrorConditions` can throw, log, or whatever. I haven't fully pursued this line of though with stuff like `yield`.I think this is a part of the point: we are able to simply write direct style, and not worry at all about the effectual context.
> how do you find the code that will run when they do fail
AFAIU, this is also the point: you are able to abstract away from any particular implementation of how the effects are handled. The code that will when they fail is determined later, whenever you decide how you want to run it. Just as, in `f : g:(A -> B) -> t(A) -> B` there is no way to find "the" code that will run when `g` is executed, because we are abstracting over any particular implementation of `g`.
What might be a good way to find / navigate to the effectual context quickly? Should we just expect an IDE / LSP color it differently, or something?
No comments yet
I believe this can be done statically (that's one of the key points of algebraic effects). It works work essentially the same as "jump to caller", where your ide would give you a selection of options, and you can find which caller/handler is the one you're interested in.
Sounds like you're just criticizing try-catch style error handling, rather than criticizing algebraic effects specifically.
Which, I mean, is perfectly fair to not like this sort of error handling (lack of callsite indication that an exception can be raised). But it's not really a step backward from a vast majority of programming languages. And there are some definite upsides to it as well.
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...
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 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.
And it is useful to be able to provide these handlers in tests.
Effects are AMAZING
I can’t have a hook that talks to a real API in one environment but to a fake one in another. I’d have to use Jest style mocking, which is more like monkey patching.
From the point of view of a React end user, there’s also no list of effects that I can access. I can’t see which effects or hooks a component carries around, which ones weren’t yet evaluated, and so on.
So conditions in Common Lisp? I do love the endless cycle of renaming old ideas
Sometimes making these parallelism hurts more than not having them in the first place
It's interesting to see how things can work if the language itself was designed to support dependency injection from the get-go. Algebraic effects is one of the ways to achieve that.
Which is why I was asking for that interesting thing to be written in the article on why it would better.