(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.
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.
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.
2. There is a much more popular and robust implementation of effects for Typescript: https://effect.website/
Naming choices matter, as does syntax.
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.
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."
"Error: "
Turns out its actually: "Error: ${msg}"
interface MovieApi {
List<Movie> getPopularMovies();
}
What are effects providing over this?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.
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.)
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.
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.
type MoviesApi = void => List[Movie]
I guess the difference is the tracking?https://lobste.rs/s/q8lz7a/what_s_condition_system_why_do_yo...
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.
artemonster•23h ago
marvinborner•22h ago
Other pages also contain some more advanced details and casestudies on effect handling
saviorand•19h ago