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}"
keeganpoppen · 18h ago
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!
keeganpoppen · 18h ago
in that same vein, “nothing runs before main” is actually an extremely based take. i suppose this is another take informed by my python (mis-)adventures, but it is so unfortunate that you don’t have just a liiittle more flexibility in terms of the order that code, imports, etc. are evaluated. it is hard for me to give a concise answer as to why i felt this pain, but it is an idea that immediately feels like respite, and gives programmers more leverage / lattitude in making libraries / other code simpler and ergonomic.
voat · 23h ago
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
mrkeen · 23h ago
I use effects to make guarantees about what the code will and won't do.
When Option was retrofitted onto Java, the NPEs didn't disappear. We got Options that could be null, but more importantly, devs (and the standard library) continued to use null liberally. We never ended up at the tautological goal of "if I have a Person, then I have a Person".
I am equally skeptical of bolting on effect systems after the fact. Even if one of these effect systems becomes 'the main one' and starts showing up in standard libraries, we'll still end up with declared-effectful code and undeclared-effectful code, but will again fall short of the goal, declared-effectful code.
gf000 · 18h ago
Doing something a bit vs almost completely can have a significant difference in practice - having a way to maybe avoid nulls doesn't solve the million dollar problem, but making non-nullability the default arguably has (see Rust, Kotlin, etc).
An even better example would be Rust's memory safety. Sure, there is still unsafe, but it being used very sparingly has reached its goal.
So I think a new language where 'pure' is the default (with a few, locally scoped escape hatches), and effects are "mandatory" would actually be pretty effective at reaching its stated goals.
IshKebab · 21h ago
I agree, this seems like something where it really needs to be natively supported in the language from day one to work properly.
sidkshatriya · 19h ago
The impact of Effects are a bit less pervasive than null wouldn’t you say ? Effects can be used with code “in between” that knows nothing about effects. Performing and handling effects can be quite transparently
epolanski · 23h ago
1. The effect data type admits an instance of monad, like arrays or option or result. Not sure why would you put those in competition, an effect is a supercharged ReaderResult. The only differences are in naming choices, traverse being called foreach, flat map being called "and then", etc.
2. There is a much more popular and robust implementation of effects for Typescript: https://effect.website/
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."
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.
vips7L · 21h ago
Maybe I’m not getting it, but isn’t this just interfaces and implementations from the OO world? For example their movie one is:
I _think_ the distinction is that the interface tells you what MovieAPI _does_, but this system also tracks what are contextual requirements to do that thing.
So when they write their version of the MovieAPI that calls out to an existing web api, they have to write `\ {Net, IO}` on the main method that uses them.
When they write their test that uses a canned recommendation, they can just write `\ {IO}`, and they have a complicated pile of test logic and confirm just at a glance at the top-level signature that no network calls are made when the test runs.
So in java, you could write two distinct classes implementing the MovieApi interface as you defined it, one of which calls out to the web and one which doesn't, and nothing about the type indicates that difference. If you accidentally used the wrong class, your tests could make a network call and you might never notice, because you would not have tracked that effect.
For someone with a java background, it's really helpful to make the analogy to Checked Exceptions, which let us propagate information about what kind of handler must be required contextually.
alethic · 20h ago
The checked exceptions analogy is a good one. Thinking of effect handlers as resumable checked exceptions with some syntactic sugar is very accurate. For someone with a Haskell background, thinking about them as "dependency injection" is also helpful (and these notions are equivalent!) but for the Java heads out there, yeah, resumable checked exceptions provides a really good mental model for what effect handlers are doing with the call stack in the general case.
tome · 7h ago
What’s the difference between a resumable checked exception and a function call?
saviorand · 6h ago
Function call always returns, and to one single caller, whereas effects can choose not to "return" at all, resume multiple times, etc
tome · 5h ago
Right, though the former is just an exception. So what general effect systems provide above and beyond what we already have in most languages is "multiply-resumable" checked exceptions (also known as multi-shot continuations and often provided by "delimited continuations").
At the time I developed my Haskell effect system Bluefin there was a conventional wisdom that "you can't implement coroutines without delimited continuations". That's not true: you can implement coroutines simply as function calls, and that's what Bluefin does.
(The story is not quite as simple as that, because in order for coroutines to communicate you need to be able to pass control between threads with their own stack, but you still don't need multi-shot continuation.)
I wasn’t aware of effects until I read the article, but I like the idea. The example you have is just an interface. Effects work a bit like boundary hooks. A pure function only works on things internal to its scope, and so it has no effects. An impure function might affect things outside of its scope, and may want to allow the caller to hook when those effects occur. Think a function that uses the gpu. That’s a side effect, and the function should be decorated to reflect that.
I’m guessing some languages would allow for checking to ensure that any induced effects are correctly noted and advertised so that the system as a whole can be more easily reasoned about.
alethic · 20h ago
It's similar on the surface. Another language, Effekt, does actually use interfaces for their effect declarations rather than having a separate `eff` declaration.
The difference comes in their use. There's two things of note.
First, the implementation of an interface is static. It's known at compile time. For any given concrete type, there is at most one implementation of MovieApi. You're using the interface, then, to be generic over some number of concrete types, by way of only specifying what you need. Effect handlers aren't like this. Effect handlers can have many implementations, actually. This is useful in the case of ex. adding logging, or writing tests to simulate I/O without actually doing it, or just having different behavior at different places across the program / call stack...
eff MovieApi {
def getPopularMovies();
}
def main() {
run {
println("Alice's movies: ", getPopularMovies());
} with handler MovieApi {
def getPopularMovies() = [
"Dr. Strangelove",
"Lawrence of Arabia",
"The End of Evangelion",
"I Saw the TV Glow"
];
}
run {
println("Bob's movies: ", getPopularMovies());
} with handler MovieApi {
def getPopularMovies() = [
"The Magic School Bus: Space Adventures",
"Spy Kids 3-D: Game Over",
"Twilight: Breaking Dawn: Part II"
];
}
}
Second, the effects of effect handlers are not functions. They're under no obligation to "return", and in fact, in many of the interesting cases they don't. The `resume` construct mentioned in the article is a very special construct: it is taking the "continuation" of the program at the place where an effect was performed and providing it to the handler for use. The invocation of resume(5) with a value looks much like a return(5), yes. But: a call to resume 1) doesn't have to happen and the program can instead continue after the handler i.e. in the case of an effectful exception, 2) doesn't have to be invoked and the call to resume can instead be packaged up and thunkified and saved to happen later, and 3) doesn't have to happen just once and can be invoked multiple times to implement fancy backtracking stuff. Though this last one is a gimmick and comes at the cost of performance (can't do the fast call stack memcpy you could do otherwise).
So to answer your question more briefly, effects differ from interfaces by providing 1) a decoupling of implementation from use and 2) the ability to encompass non-local control flow. This makes them not really compete with interfaces/classes even though the syntax may look similar. You'd want them both, and most effectful languages have them both.
KPGv2 · 17h ago
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.
vips7L · 16h ago
An interface is just a function signature. Perhaps it might be easier for you to think about as just a function:
type MoviesApi = void => List[Movie]
I guess the difference is the tracking?
LudwigNagasena · 17h ago
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.
mrkeen · 23h ago
> 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.
gf000 · 18h ago
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.
epolanski · 23h ago
This depends on the implementation. In effect typescript land, synchronous and asynchronous code is treated equally as soon as it's lifted in an effect.
siscia · 19h ago
This is absolutely neat.
artemonster · 1d ago
"The only one that I know of besides Flix is Unison." huh? Koka, Effekt
Other pages also contain some more advanced details and casestudies on effect handling
saviorand · 19h ago
Koka, Effekt and 4 other research languages are mentioned earlier in the article though? This talks about practical languages, not research-only
artemonster · 1d ago
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.
Sharlin · 23h ago
I’m sure default effects could handle this, just as Rust has some auto traits and default trait bounds. It doesn’t even have to be the full console i/o effect, it can be a specific debug effect that outputs to console in a dev build and to a file or devnull or whatever in release builds, or you could disable it in release builds and the compiler ensures there aren’t stray debug calls left, without needing a specific lint for that.
(Rust does have a similar problem in that debug printing requires the Debug trait bound which can be really annoying when writing generic code. Would be nice if there were a sprinkle of RTTI involved.)
Warwolt · 1d ago
To be fair, presumably debug printig could be "escaped" from the effect type checking if the designer of an effect system would want it. For instance, debug printig in Haskell completely sidesteps the need for the IO Monad and just prints in whatever context
artemonster · 1d ago
yeah, most times its solved by side-stepping the strict type system and making an exception for debug prints. but this is not a real practical solution, this is a stupid workaround born from overinsistence on "beautiful" design choices.
thfuran · 23h ago
It seems to me like a pragmatic compromise and very much a real solution. What would you consider a real solution that isn’t overinsisting on beautiful design choices?
artemonster · 23h ago
putting strong static type system into optional compiler pass. yes, I know this may be null is some cases, let me run my program for now, I know what I am doing. yes, there are unhandled effects or wrong signature, just let me run my test. yes, that type is too generic, i will fix it later, let me run my god damn program.
noelwelsh · 23h ago
This puts a lot of extra conditions on the runtime; you basically have to implement a "dynamically typed" runtime like Javascript. In doing so you lose a lot of performance. Google have invested something like a century of man-hours into V8 and on typical benchmarks the performance is about half of Java's, which in turn is typically about half of C / Rust's performance. That's a pretty big compromise for some.
gf000 · 18h ago
Well, you can reuse all that man-hours that went into the JVM - this is what Flix does!
epolanski · 23h ago
1. Nonsense [1]
2. It's implementation dependent, but of course you lose tracing, etc if you want to just log with language primitives, which is why you shouldn't when every effect system offers you tools to do so. If you want a system where each side effect is traced, monitored and encoded as a recipe, then you use the effectful version.
on 1: i want to stab my eyes out. you cant surely implement any monadic and/or effect system on top of any native generators and effect.ts is no exception, but this is just so wrong, buegh
on 2: can you please reformulate and elaborate more? i have re-read what youve said 5 times and it feels like gpt written rambling, sorry
epolanski · 8h ago
Yes you can, why wouldn't you? It's an implementation detail in any case.
It's fine if you don't understand, when people comment about topics they don't know much about, such as effect systems in your case, it happens.
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.
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.
When Option was retrofitted onto Java, the NPEs didn't disappear. We got Options that could be null, but more importantly, devs (and the standard library) continued to use null liberally. We never ended up at the tautological goal of "if I have a Person, then I have a Person".
I am equally skeptical of bolting on effect systems after the fact. Even if one of these effect systems becomes 'the main one' and starts showing up in standard libraries, we'll still end up with declared-effectful code and undeclared-effectful code, but will again fall short of the goal, declared-effectful code.
An even better example would be Rust's memory safety. Sure, there is still unsafe, but it being used very sparingly has reached its goal.
So I think a new language where 'pure' is the default (with a few, locally scoped escape hatches), and effects are "mandatory" would actually be pretty effective at reaching its stated goals.
2. There is a much more popular and robust implementation of effects for Typescript: https://effect.website/
Naming choices matter, as does syntax.
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."
https://lobste.rs/s/q8lz7a/what_s_condition_system_why_do_yo...
So in java, you could write two distinct classes implementing the MovieApi interface as you defined it, one of which calls out to the web and one which doesn't, and nothing about the type indicates that difference. If you accidentally used the wrong class, your tests could make a network call and you might never notice, because you would not have tracked that effect.
For someone with a java background, it's really helpful to make the analogy to Checked Exceptions, which let us propagate information about what kind of handler must be required contextually.
At the time I developed my Haskell effect system Bluefin there was a conventional wisdom that "you can't implement coroutines without delimited continuations". That's not true: you can implement coroutines simply as function calls, and that's what Bluefin does.
(The story is not quite as simple as that, because in order for coroutines to communicate you need to be able to pass control between threads with their own stack, but you still don't need multi-shot continuation.)
I’m guessing some languages would allow for checking to ensure that any induced effects are correctly noted and advertised so that the system as a whole can be more easily reasoned about.
The difference comes in their use. There's two things of note. First, the implementation of an interface is static. It's known at compile time. For any given concrete type, there is at most one implementation of MovieApi. You're using the interface, then, to be generic over some number of concrete types, by way of only specifying what you need. Effect handlers aren't like this. Effect handlers can have many implementations, actually. This is useful in the case of ex. adding logging, or writing tests to simulate I/O without actually doing it, or just having different behavior at different places across the program / call stack...
Second, the effects of effect handlers are not functions. They're under no obligation to "return", and in fact, in many of the interesting cases they don't. The `resume` construct mentioned in the article is a very special construct: it is taking the "continuation" of the program at the place where an effect was performed and providing it to the handler for use. The invocation of resume(5) with a value looks much like a return(5), yes. But: a call to resume 1) doesn't have to happen and the program can instead continue after the handler i.e. in the case of an effectful exception, 2) doesn't have to be invoked and the call to resume can instead be packaged up and thunkified and saved to happen later, and 3) doesn't have to happen just once and can be invoked multiple times to implement fancy backtracking stuff. Though this last one is a gimmick and comes at the cost of performance (can't do the fast call stack memcpy you could do otherwise).So to answer your question more briefly, effects differ from interfaces by providing 1) a decoupling of implementation from use and 2) the ability to encompass non-local control flow. This makes them not really compete with interfaces/classes even though the syntax may look similar. You'd want them both, and most effectful languages have them both.
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.
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.
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.
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.
Other pages also contain some more advanced details and casestudies on effect handling
(Rust does have a similar problem in that debug printing requires the Debug trait bound which can be really annoying when writing generic code. Would be nice if there were a sprinkle of RTTI involved.)
2. It's implementation dependent, but of course you lose tracing, etc if you want to just log with language primitives, which is why you shouldn't when every effect system offers you tools to do so. If you want a system where each side effect is traced, monitored and encoded as a recipe, then you use the effectful version.
[1] https://effect.website/play#769a55e0ea0a
It's fine if you don't understand, when people comment about topics they don't know much about, such as effect systems in your case, it happens.
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.