21 comments

[ 2.2 ms ] story [ 53.6 ms ] thread
"The only one that I know of besides Flix is Unison." huh? Koka, Effekt
Koka, Effekt and 4 other research languages are mentioned earlier in the article though? This talks about practical languages, not research-only
I really want effects to shine and thrive, but since this is a very academic topic, only academic people engage with the research and it comes with academic pedantism, perfectionist worldview, strange attraction to beautiful consistency, etc. This automatically places effect research FAR from any practicality and grounded realism. 2 points, as examples: 1. Ever tried adding a simple print statement for debugging purposes while coding in effectful lang? compiler: "NNNOOOOO!!!! THIS IS AN ERROR; I WILL NEVER COMPILE THIS NONSENSE YOU __MUST__ SPECIFY CONSOLE EFFECT WAAARGHH!11" 2. multi-resumable stacks. how many times you really want to implement custom backtracking for multi resumable computations? this is such an obscure nonsensical use case that is hard to nearly impossible to solve efficiently, but everyone wants to support it. supporting this adds enormous complexity and kills any potential for performance. WHYYYYYYYYY. and yet they focus on this as a holy grail feature but whever there is a showcase they use synthetic "choice" example, lol.
The Unison language supports algebraic effects and optimizes handlers that call their continuation at most once in tail position (we call these "affine"), so you can have the best of both worlds. Some links at the end if you're curious.

Here are a few places where "multi-resumable" stacks are still useful, even outside of nondeterminism:

* For instance, in a workflow engine a la Temporal, a `sleep` primitive might serialize and store the continuation in a distributed priority queue. The workarounds of not having access to the continuation are all not nearly as good.

* A pure interpreter of a structured concurrency ability is quite useful for testing, since it can test different interleavings of threads and produce tests that fail or pass deterministically. For Unison Cloud's distributed programming API, we have an (in-progress) chaos monkey interpreter that you can use for local testing of distributed systems.

* You can implement a simple debugger... as a library. It lets you set breakpoints and go forwards and backwards in time. Here's an example: https://share.unison-lang.org/@pchiusano/stepwise

Basically, any time you want to stash and do something interesting with the continuation, even if you only end up ultimately using it once, you still need the more general form of algebraic effects.

And then there are various nondeterminism effects that do call the continuation more than once. I'd say these are somewhat niche, but when you need them, you need them, and the code comes out much nicer. I especially like it for testing. You generally want tests to just be the logic, not a bunch of looping code or map/flatMap.

Some links:

* https://www.linkedin.com/posts/pchiusano_dan-doel-has-been-d... has some details on the optimization we do in Unison

* https://dolio.unison-services.cloud/s/blog/posts/optimizing-... is Dan's blog post on optimizing affine handlers

* https://www.linkedin.com/posts/pchiusano_kestrel-is-a-higher... is a typed query DSL that uses nondeterminism in an interesting way. For a declarative query DSL, it's nice to avoid explicit looping, similar to what SQL does.

Resumable code is extremely helpful and I'm pretty sure you're describing a chicken and egg issue: if resumable code was readily available in general, people would use it a lot more and it would look like a very normal tool that is often the best.

But when it's only a feature available in niche languages or many languages make it unsafe or unreliable, then very few people will get to the point where it naturally pops up in their mind as a solution to their problem.

> Solving the “what color is your function” problem.

Eh. I currently use monads for my effects, and am open to the idea of language-based effects.

But this isn't a solution to the colour problem - it is the colour problem. The compiler stops you accidentally calling red from blue. If you want to do it deliberately, you change the caller from blue to red.

One important thing that languages with algebraic types can usually do and most others can't is being generic in this "colorness" - precisely because this is the color problem they are solving.

So they can e.g. have a `map` function that can take a pure lambda, or an effectful one, and based on that itself will become pure or the given effect. Colors will stop being "infectious" without limits, you can now better barrier their reach.

The algebraic effect handler pattern is a much simpler mental model than monads. And is transferrable to other languages pretty easily. See something like https://github.com/doeixd/effectively in Typescript
OCaml got experimental algebraic effects in version 5.0 (December 2022). Here is a tutorial: https://github.com/tanelso2/ocaml-effects-tutorial

Also from the tutorial: "Unlike Eff, Koka, Links, and other languages that support effect handlers, effects in Multicore OCaml are unchecked currently. A program that does not handle a performed effect fails with a runtime error."

For anyone using a dark theme with their browser or OS, the string constants actually have interpolation syntax in them. It's just black, on a black background. For example, I thought it was strange their error message was:

    "Error:       "
Turns out its actually:

    "Error: ${msg}"
Maybe I’m not getting it, but isn’t this just interfaces and implementations from the OO world? For example their movie one is:

    interface MovieApi {
        List<Movie> getPopularMovies();
    }
    
What are effects providing over this?
Implementing an interface results in a class that implements it. From then on, the fact your class implements that interface is entirely obscured from your code. In the case of tracking effects, this is clearly undesirable. (Also, in a language that doesn't have classes, implementing interfaces is difficult for me to think about.)

In the case of algebraic effects, they're functions with type signatures explicating the effects the function has. Those don't go away until somewhere nearer the edge of your program, outside the core, you say "by the way, use this to handle those effects"

Effect handlers also give you control over continuations so you can sequence effects explicitly, which is—speaking as someone who writes a lot of code in a language that has algebraic effects as a core part of the language (Unison)—really powerful. Deciding in some instance how you prioritize IO vs Exceptions, making a thrown error go away and replacing it with an HTTP effect that, wayyyy far away from this code handles all HTTP calls by attaching a self-signed certificate when doing the SSL handshake, or whatever, is very nice.

The more I write in this style, the more interesting techniques I come across.

Flix does not have generic effects, yet it has generic traits.

This hints that effects are not meant to deal with data variability.

Effects are allowing variability of control flow.

Abstracting `try/catch/finally` or `async/await`.

For example, something like "durable workflows" is just a specific effect implementation, code stays the same.

True, although I think they're working on support for generic effects
Are algebraic effects a recreation of common lisp’s condition system?
This is absolutely neat.
wait, associated types are amazing! i have spent more time than is healthy goofing around with doing more things than is probably sane via metaclasses, and this concept would actually make life SO much cleaner… i was already predisposed to liking this post because i feel strongly that algebraic effects (and delimited continuations) are amazing and being slept on, but having poked around the docs and blog a bit i am beyond impressed. baasically every “choice” made with this project is one that i either agree with or i’ve never seen before and is quite clever. for example, putting polls in the middle of blog posts to ask the reader what kinds of things would be most relevant for them to touch on in future posts. maybe others have done it before— i don’t know— but what a great way to run a programming language project, where i can only imagine that trying to get in the minds of actual or prospective users is one of the hardest tasks— probably more so than the implementation and development of the language itself! will be watching this project for sure!
There are some parallels with Zig's current move toward an IO-object, and requiring it to be passed to any function that wants to do IO (so that it can call methods on that object). Admittedly, it's a more limited and less elegant implementation, but it's a small step in this direction.
I think it is a cool feature, but I see some practical issues.

For me, encapsulation is a feature. I would like to see that a function uses a network call deep down, but only in a static analysis sort of way. So I don't want to mark something as potentially using a network until it actually uses it. And at the same time I wouldn't want to change every effect of every intermediate function just because I made something use an LLM or Redis cache.

It also seems that cross-cutting components such as observability instruments are just going to contaminate all functions with their need for memory, network, io, files, clock, locale, reflection, etc.

First, you can write a function that's generic on the effects used, so you could write use it in a way that calls an LLM and use it elsewhere in a way that cannot, with the same logic.

But I see the need to change the code as a good thing when it is necessary: I don't want a dependency, inside or outside my codebase, to suddenly add network calls without me knowing it. Same goes for code that was pure and has become stateful. Not knowing this kind of fundamental change is a recipe for a nightmare.

It should be especially apparent with the recent supply chain attacks.

In the real world you usually want to extend datatypes and provide new implementations without recompiling let alone rewriting old code (The so called expression problem). This is the raison d'être of complex DI and IoC enterprise patterns and various monkey patching techniques.

I am not sure how flexible the effect approach is. It took decades before static typing became painless enough for rapid agile development. Properly typed DI with safeguards sounds wonderful, but can algebraic effects actually deliver that?