Live data from Hacker News

Algebraic Effects for the Rest of Us

overreacted.io

11–20 of 106 posts

Re: Algebraic Effects for the Rest of Us

#11
post #6

I doubt if you need any new language construct to introduce this. Could you not simply pass an error handling function/object with all your functions/methods, which is called when an error occurs? This function/object could then resolve the error or throw an exception if it cannot resolve the error. It is possible, and relatively easy, to chain such error handling functions/methods to implement complex error handling…

The article only gives a limited example of algebraic effects. The thing that is required for a condition system (ie good resumable exceptions) is lexical non-local transfer of control and doesn’t need algebraic effects (Common Lisp had a condition system but not algebraic effects in the late 1980s).

Lexical non-local transfer of control is effectively the ability to write code which looks like (made up JS like syntax):

  function find(haystack, needle) {
    iterate_big_datastructure(function(x) { if (x == needle) goto found; });
    return false
    found:
    return true
  }
And allows unwinding the stack to lexically scoped labels (whereas exceptions transfer control to dynamically bound labels). This is reliable because the stack-unwinding can’t really be stopped like it can with an exception.

This then allows conditions to be implemented by dynamically binding a list of condition handlers (functions which decide what to do when there is a condition) and restarts (functions which do the thing by non-locally transferring control out of themselves). Then instead of raising an exception and unwinding the stack, one signals a condition by creating the condition object, computing the set of restarts, and asking each handler in turn what to do until a handler invoked a restart which will unwind the stack to the right place to resume.

So one might ask how conditions (or rather lexical goto) are different to algebraic effects. The difference is that all these do is let you unwind the stack to specific places using closures and syntax to give the impression that control flow briefly jumps up the stack and back. Algebraic effects instead give you delimited continuations: when an effect is performed, the handler is given a continuation which is a bit like a return pointer plus the bit of the stack between the handler and the place the effect was performed, packaged up to look like a function. This means that one can put these continuations into data structures and do other things, effectively forking the stack. Lexical goto doesn’t let you implement something like async/await but algebraic effects do.

As a diagram, here is a schematic of the stack in the two language features. We’ll try to write down stack frames with dots, a bar for a condition/effect handler and > for the top of the control stack.

  Condition system
  ........|..........>
    Condition signalled
    Call closure created by function at |
  ........|...........>
    Find and invoke restart
    e.g.
  ........|......>
    or
  ......>
  
  Effect system:
  .........|...........>
    perform effect
             ,.........*
  .........|
     stack is now “forked”, control is at the effect handler
     It can return control back to *, invoke another function (leaving the stack forked),
      return up the stack (invalidating the continuations),
      or otherwise transfer control (e.g. perform another effect).
A final question is how algebraic effects are different from having delimited continuations and building an effect system on top. The answer is that they work in strongly typed languages where one must know what type will result from performing an effect and be sure that all a functions effects will be handled.

Another way to do these effect like things is with monads which effectively convert one’s code into continuation passing style. These can make things slow if the compiler doesn’t like inlining/allocating/calling closures. Then one can have programs to evaluate these monads which work like the effect handlers because the program is already set up to put its continuations into the monad. It is hard to compose multiple monads (in particular if one doesn’t have typeclasses or a MTL equivalent) but it looks like algebraic effects will be more composable.

Re: Algebraic Effects for the Rest of Us

#14
post #6

I doubt if you need any new language construct to introduce this. Could you not simply pass an error handling function/object with all your functions/methods, which is called when an error occurs? This function/object could then resolve the error or throw an exception if it cannot resolve the error. It is possible, and relatively easy, to chain such error handling functions/methods to implement complex error handling…

The article only gives a limited example of algebraic effects. The thing that is required for a condition system (ie good resumable exceptions) is lexical non-local transfer of control and doesn’t need algebraic effects (Common Lisp had a condition system but not algebraic effects in the late 1980s). Lexical non-local transfer of control is effectively the ability to write code which looks like (made up JS like synta…

Question about forking: isn't this essentially the same thing as restarts, after the garbage collector gets rid of the original (and now dangling) stack branch? Or is there something you can do with both of these branches simultaneously that's not expressible in conditions/restarts system?

Re: Algebraic Effects for the Rest of Us

#15
post #9

Maybe I'm missing something, but isn't this essentially just coroutines? In Lua you can do `myFile = coroutine.yield("get a file")` to pause your coroutine, and when the caller does `coroutine.resume(someFile)` it's resumed with the value passed in. EDIT: I guess the difference would be in Lua, yields return to where they were resumed each time, while in algebriac effects they return to the nearest handler for that c…

JS's generator functions have a yield operator that that works in a very similar way - a function can 'pause' and return a value and then resume from the same place the next time it's called. I think that's closer to Lua's yield than the effect Dan is talking about in the article.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Re: Algebraic Effects for the Rest of Us

#17
Now conditions/restarts. How much more decades should we wait till they rediscover all the programming tools? Please, rewrite your browser legacies once to support non-trivial execution model and allow any decent language to stop this madness.

Re: Algebraic Effects for the Rest of Us

#18
post #7
post #6

I doubt if you need any new language construct to introduce this. Could you not simply pass an error handling function/object with all your functions/methods, which is called when an error occurs? This function/object could then resolve the error or throw an exception if it cannot resolve the error. It is possible, and relatively easy, to chain such error handling functions/methods to implement complex error handling…

The examples provided in the article aren't especially compelling. These are capable of among other things, Prolog style nondeterministic choice and backtracking. A more in depth introduction is here: https://www.eff-lang.org/handlers-tutorial.pdf (not really for 'the rest of us' - I struggled to understand this one, but found it interesting).

Koka's docs are also okay, but could still do with some more work to help explain things in a compelling way: https://koka-lang.github.io/koka/doc/kokaspec.html

Re: Algebraic Effects for the Rest of Us

#19
> The best we can do is to recover from a failure and maybe somehow retry what we were doing, but we can’t magically “go back” to where we were, and do something different. But with algebraic effects, we can.

This is literally Common Lisp's condition system which a) decouples signaling conditions from handling conditions, b) allows you to execute arbitrary code in the place that signals, c) allows you to stack independent pieces of condition-handling code on one another so they run together, and d) allows you to unwind the stack or not unwind the stack at any moment, so your code may continue running even if it ends up signaling.

ANSI Common Lisp was standardized in 1994. This post is from 2019. That's reinventing a 25+ year old wheel the author is likely unaware of.

Re: Algebraic Effects for the Rest of Us

#20
post #15
post #9

Maybe I'm missing something, but isn't this essentially just coroutines? In Lua you can do `myFile = coroutine.yield("get a file")` to pause your coroutine, and when the caller does `coroutine.resume(someFile)` it's resumed with the value passed in. EDIT: I guess the difference would be in Lua, yields return to where they were resumed each time, while in algebriac effects they return to the nearest handler for that c…

JS's generator functions have a yield operator that that works in a very similar way - a function can 'pause' and return a value and then resume from the same place the next time it's called. I think that's closer to Lua's yield than the effect Dan is talking about in the article. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Except that in js all of the call stack must be marked as generators. Lua thread can yield through any careless function, iterator and even C routine (if the latter does a simple continuation trick).
Post reply on HN