Why Algebraic Effects?

39 points by osa1


typesanitizer

I’m not super sure what the intended target audience of the blog post was, but assuming that it includes (1) programmers from coming from both FP and non-FP languages (given the example of allocation) and (2) regular day-to-day programmers in addition to language implementers (given the example of DB queries), I find this falls into the typical trap of arguing for one side of a feature without deeply examining the other side.

In particular, there is little examination of how people accomplish the same things today in other languages which lack algebraic effects (i.e. most of them), and what that experience is like, and how this improves upon that.

I hope this doesn’t come across as mean; I don’t want to discourage the author, but I feel this frustration on reading basically every other post on algebraic effects, where people keep repeating similar points without closely examining them.

If you look at the concrete examples:

  1. The DB example is an interesting one. But the point here is really about programming against interfaces (which can be easily substituted) instead of implementations. Today, this is often done by using interface types or similar (and making the syntax sufficiently lightweight). In languages such as Go and Swift, the syntax is very light (just InterfaceName or any ProtocolName) which makes this appealing.
    • So it’s unclear what the value add of effect handlers is here over plain parameter passing.
  2. The logging example is a good one. In our Go codebase at work, we generally stored the logger in an observation.Context and pass that down. It works OK. If you want effect safety, you need to annotate your functions anyways. So the ‘plumbing code’ is ~roughly the same, modulo minor syntax differences.
    • So it’s unclear what the value add of effect handlers is here over plain parameter passing.
  3. The example of the ‘Use a’ effect is a bit odd. From a library POV, why is it better to have can Use Strings rather than an extra mylib::Context parameter?
    • The ‘automatic threading’ means that code nav becomes a bit trickier.
  4. The PRNG example seems just wrong? It states “We would have to require users to explicitly thread through the Prng object… Why should such a small implementation detail cost so much to the terseness of the program? If we want to avoid this, we may make the Prng a global … If we make an effect … We gain the ability to thread it through a program mostly for free”
    • Is the point is that by annotating the signatures, the compiler will do type-based matching for which function requires which effect and then plumb down those values automatically…? If so, I suspect this doesn’t really address the main point for why people don’t like this, it’s because it still requires modifying the function signatures for every function in the call chain… (in contrast, plumbing the arguments is straight-forward; I’d hazard a guess that if you use an LLM-based auto-complete tool, it can probably “predict” how the plumbing should happen to make this kind of editing very fast). You also need to think explicitly about wiring when concurrency gets involved.
  5. For the ‘Allocate’ example:
    • From a language user POV, if existing languages are anything to go by, for the majority of people using most languages, apart from Zig, people really do not want to have to mark every function that allocates. Odin presents an interesting point in the design space where it will let you set a temporary allocator on the magic context parameter, which gets implicitly passed to all functions unless one explicitly opts out.
    • From a language implementer POV, the other downside is it’s not obvious how to fit memory safety (if you care about that) into a language which supports such an allocation effect.
  6. Record-replay debugging already works today with rr, and there are lower overhead options for specific languages such as Replay.io for JS, without requiring effect handlers.
    • Is the argument here about ease of implementation?
  7. The point about Capability-based Security is a bit of a red herring. Being able to enforce capability-based security requires you to be able to guarantee that the called function does not use ‘ambient authority’. Effect handlers are neither necessary nor sufficient for this. Ambient authority stems being able to use things which are not parameters – chiefly mutable global variables (accessible in-language, or via FFI) or their cousins (thread-local storage, task-local storage etc.).
    • For language users today, you can get this by creating a style guide + a linter that enforces this property for the code you care about.
    • For language implementors, you need to focus on the restrictions around mutable global variables etc. (you need this anyways if you have capability-based security, regardless of whether you have effect handlers).