FR version is available. Content is displayed in original English for accuracy.
Advertisement
Advertisement
⚡ Community Insights
Discussion Sentiment
67% Positive
Analyzed from 3811 words in the discussion.
Trending Topics
#effect#effects#algebraic#function#example#https#type#system#using#don

Discussion (72 Comments)Read Original on HackerNews
But then at some point it struck me: underlying all these effect systems is just passing stuff in. So I developed my own effect system for Haskell, Bluefin[1], based on capabilities, which means the "capability to perform some effect" is represented by just passing stuff in (that is, a function can do some effect as long as it has been passed the capability to do it).
From this point of view it's hard to understand the excitement over "resume with" and "the part you can’t do with try / catch. It lets us jump back to where we performed the effect, and pass something back to it from the handler". Programming languages have had that feature since forever: a "resumable exception" is a "function call". A dynamically chosen "resumable exception" is the call of a dynamically chosen function, i.e. the argument to a higher order function.
So I don't know why people love the complexity around "algebraic effects". Maybe the mystique has a certain allure. But if you want the most straightforward possible approach I can recommend you try out Bluefin. I'm happy to answer questions on the issue tracker[2].
(Caveat: Bluefin is able to simplify things dramatically by dropping support for "multi-shot" continuations. But mostly you don't want multi-shot continuations.)
EDIT: I was too pessimistic, bazoom42 has noticed this :) https://news.ycombinator.com/item?id=48334067
[1] https://hackage.haskell.org/package/bluefin
[2] https://github.com/tomjaguarpaw/bluefin/issues/new
> A real effect system allows you to do things like NOT continue execution after using the effect
Right, Bluefin's Request allows you to do that too. For example here is an example of handling the request by continuing or not, depending on what the value yielded to the Request is.
> if you "implement" this by using Exceptions, you're not using effects at all, just using Exceptions with extra stepsNot sure I follow that. Above you can see I used an exception (Bluefin's Throw capability), but I couldn't have used only an exception because that would have aborted unconditionally. What am I missing here, that makes "using Exceptions" "not using effects at all"?
> only continuing it after some asynchronous work happens (the Future effect)
I'm not really sure what "a Future effect" is, but I don't see how it's not something that can be run as a function call, at least in Haskell.
> or even "continue" execution several times
Right, these are the multishot continuations which Bluefin doesn't support. I haven't discovered many particularly compelling use cases for multishot continuations but would be very interested in finding some. The developer of the Kyo effect system for Scala, Flavio Brasil, suggested parsing, with multiple parse results, which makes sense.
I'm also not entirely sure Bluefin couldn't simulate common use cases of multishot continuations with threads, but I haven't thought about it very hard.
> You still don't seem to have understood effects.
Possibly true, and part of my puzzlement! I'm always happy to try to improve my understanding. Can you help me see what I've missed?
The only mystique around algebraic effects is the same mystique there is around monads. I don't know if people have started equating algebraic effects to burritos yet but that's a pretty good way to take something simple and turn it into something confusing.
Fair enough. But are you responding to something I said? I didn't make that equation.
> The only mystique around algebraic effects is the same mystique there is around monads. I don't know if people have started equating algebraic effects to burritos yet but that's a pretty good way to take something simple and turn it into something confusing.
Ah, are you saying that fundamentally there isn't really much to algebraic effects and they're much simpler than they're made out to be? If so then it perhaps we agree?
I submitted this because I've been getting really interested in effect systems, especially now that OCaml 5 has a working production quality example they'd been iterating on for years prior. I wanted to see what it'd look like in Rust too so maybe one day we can get rid of async function coloring, and with OxCaml by Jane Street maybe we could see how that would look in practice.
Another reason for submitting this is that React actually has a quite robust effect system, that people don't necessarily realize they're using one every day if they use hooks.
It’s worth clarifying that for the most part, this article just discusses plain effects, ie. "reified side effects" or "resumable exceptions". Algebraic effects are about the composition (ie. algebra) of effects, exactly like algebraic data types are about the composition of types. This part is generally not meaningful in an untyped language like Javascript because effects are all YOLO and you never know what’s going to happen, what effects a function might throw, or whether there’s any handler up-stack to catch your effect.
Contrary to the claim from the comment you are replying to, according to Andrej Bauer, "algebraic" does not refer to the composition of different algebraic effects via composing their handlers.
When you see "algebra," think "algebraic structure," i.e., a set of operations and a set of equational laws on those operations.
An algebraic effect consists of a set of effectful operations. In that sense, each algebraic effect (reader, state, etc.) defines its own algebraic structure. In theory, the operations of an effect should also be related to each other by laws. Here is an example for the state effect from Andrej Bauer's paper:
Therefore, the state effect has an algebraic structure.However, monads that cannot be defined in this equational style cannot be translated to algebraic effects. An example is the continuation monad: https://old.reddit.com/r/haskell/comments/44q2xr/is_it_possi... (I think you are involved in the Reddit comment chain that I'm linking to...)
AFAIK, another example is the "exception" monad, because the way that you interact with it is through the handler itself. I once saw a thread on r/Haskell discussing this, but can't find it.
I show it when I teach Haskell, and it's what usually makes it "click" for students. Probably because motivating examples are in normal imperative pseudocode.
The wiki entry on effect systems[0] tells me that a focus of an effect system is something different from a focus of monads. "The term algebraic effect follows from the type system", where an effect system is effectively a type and effect system. It links to Monadic encapsulation of effects[1] and mentions the runST monad when it mentions support in Haskell, as that one seem to "simulate a type and effect system".
Do have any such a link on the runTS monad?
[0]: https://en.wikipedia.org/wiki/Effect_system
[1]: https://www.cambridge.org/core/journals/journal-of-functiona...
> Algebraic effects & handlers use a free monad and an interpreter to separate the syntax of effects from their semantics
I'm pretty sure not everybody who works with algebraic effects would say they have to be based on a free monad, so I'm skeptical how definitive this definition is.
You need stackful coroutine (like goroutine) for that.
And so then the outer code is a loop around coroutine.resume, and the inner code uses coroutine.yield to perform an effect.
[a]: https://github.com/saucisson/lua-coronest
https://lisp-docs.github.io/cl-language-reference/chap-9/j-b...
But while the condition system can do many things you can also do with effects, they cannot do everything.
Here's another discussion on this: https://news.ycombinator.com/item?id=44078743
Nondeterminism is not a feature you want. Algebraic effects treat the execution stack (continuation) as data, you have total freedom over what you do with it. This flexibility is exactly where you get nondeterminism. This is how logic solvers or probabilistic algorithms work, but you don't want it as a programming language feature in general purpose programming language.
By the way, nondeterminism is not the only difference between the two.
I can get behind the sentiment, but you absolutely need nondeterminism. You can separate the d from the non-d, but only Haskellish languages even attempt it. It's a coarse separation to make (IO vs non-IO), which is where effect systems come in - I guess you can categorise code into more fine-grain buckets. The 'algebraic' part is currently beyond my knowledge.
No ASM involved so technically portable (although it depends on built-in).
Flix equivalent (copy paste to https://play.flix.dev/):
When I had been in university twenty years ago, I had constructed a language with "effects" as I had understood them then: side-effects annotations. I could not in my head recognise how the new effects were like those I had read about in the literature back then.
More recently, I've been working on a compiler back-end/runtime in my (too much) free time with support for resumable exceptions. I didn't know it before, but after reading this article, it appears that the runtime actually does have support for "effects", without me really trying.
In practice, what that means is you get a strong temptation to hang behaviour on data even when it doesn't fit perfectly. Because of the natural desire to reduce the number of entity definitions, you end up defining a typeclass on a data type that doesn't fit exactly just to get the behaviour to the right place without having to introduce a new policy or something.
Effects change this by essentially letting you provide multiple names implementations for the same data type, and you don't need to pass around a policy type because the polymorphism lets you tie handlers to scope.
So, if the fancy type-safe library-based control flow doesn't really do much for you, I think that their potential for code design is a good reason to still be excited!
I was wondering how well TypeScript can type generator-based effects. My hypothesis is that TypeScript can let you compose functions with effects, but it is not possible for it to narrow down that the result from an effect corresponds to the effect (i.e. the result of an effect will be any possible effect result used by the function).
My incomplete, untested attempt in TypeScript[0] tries to implement `enumerateFiles` and `withMyLoggingLibrary`. The type errors demonstrate TypeScript's limitation that it can't associate an effect call with its result.
[0]: https://www.typescriptlang.org/play/?#code/C4TwDgpgBAMg9gcwK...
https://www.unison-lang.org/
https://flix.dev/
But you can have generic effects. Your arguments and return type can specify "any effect", indicating your function can use a type with any effect safely, or can be used in any effect context safely.
Passing an async value to a function doesn't mean that function must now also be an async function. It can be a "for all effects, do the thing" function. The code duplication problem is gone.
Someone writes a post lamenting red and blue functions, and everyone eats it up.
Substitute colour for something meaningful and the idea becomes idiotic.
"Top level function declares that it is non-blocking, but when I try to call a small blocking function from it, I have to change the declaration to blocking???"
Yes, yes you do.
Total functions can't call non-total functions.
Deterministic functions can't call nondeterministic functions.
Non-IO functions can't call IO functions.
How do you handle logging then? If f() calls g(), how can I add logging to g() without having to change or recompile f() (and everything in the call stack above it)? ‘You can’t’ is not an acceptable answer.
Yes, but every every control flow statement in programming is more or less a fancy goto.
If, do, while, for, try/catch, ...
I've been using it for 5+ years and my 4 men team can scale to supporting 6 different products (each running millions $ in business, sometimes daily), as we reuse the same patterns and architecture. This would not be possible without Effect, even though I'm lucky to have terrific engineers as colleagues, we just wouldn't be able to without the endless goodies from Effect.
The amount of features is basically endless, as effects and runtimes weren't enough, from SQL to AI, from effectful schemas (encoders/decoders), first-class OTEL support, CLI, debuggers, editor extensions, and many others. There's still countless modules I have yet to see or use.
Runtimes are available for each platform, including cloudflare workers.
There's absolutely nothing in TypeScript land to have such a wide scope.
v4 will also bring durable workflows (I'm already using v4 beta and that feature in prod) and many other goodies. That's quite important for us needing to have procedures that need to survive redeploys, crashes, etc.
I would never go back to writing standard TypeScript.
There is a learning curve, but you can adopt it incrementally. Nobody adopting it has ever gone back.
That being said, it would be great if there was a proper effect-based language (I've seen few projects like Effekt, but there's way too many things missing) as TypeScript is verbose, and effect adds its own verbosity.
[1] https://effect.website/
I know parts of Effect like its schema are incrementally adoptable but if you use it substantially with many of its features, isn't it viral in a sense? In that you need to do things the Effect way and wrap libraries into Effect functions?
E.g. you could describe a complex effect that has retry, scheduling, etc and run it only once with `Effect.runFork(yourEffect)` in a random place of your existing code.
That's in general how teams adopt it, in general there's a champion in the team that sells using one feature, and as people get accustomed and the champion does a good work mentoring it slowly takes over whole projects.
Then I heard about Effect-ts, checked the doc, and realize that Effect-ts already has all these things, in a single package.
IMO Effect-ts is currently the most practical effect system right now. While it does not support resuming like in other algebraic effect languages, it is powerful enough to express common patters, but not too powerful so that the code becomes hard to understand.
I hope effect-ts gets more traction. The biggest obstacle to sell it right now is that the API doc is not great. I had to trace the source code several times just to see how a type is defined. I hope Effect-ts team is aware that more people will use it if it has a proper documentation.
I think you've missed the point regarding effect systems. Concurrency and resource handling, implemented in a way that is composable and reasonably easy to reason about, are two of the big ticket features.
I was already in the camp that try/catch is "considered harmful", I dislike the concept of having a second, hidden, control flow that might get sprung up upon function callers, because it has side effects buried in the implementation of a callee that are not defined in the parameters or the returns, and I am not 100% sold on the benefits of "Things in the middle don’t need to concern themselves with error handling.", which I guess informs this opinion.
Now since I hate that, I really, really would hate that on top of this, another programmer could write a hidden control flow upstairs that could, potentially, not just crash my code, but also do a lot of other things, such as coming up with default values for unexpected NULLs or whatever, which could THEN take something that would have crashed immediately, and turn it into something that crashes later down the line, away from the problem, with a varialble set to an inexplicable value that I have never put there myself
What a nightmare to debug! I mean, come on
The latter is very important because in your example it would not really be hidden. If your function does not have the "exn" effect, it cannot call functions that throw exceptions, full stop. Same with any other effect including IO if you want.
Basically function coloring taken to the extreme. In a statically typed language with statically typed effects you actually cannot get surprised, which was your major complaint.
Type systems that support algebraic effects also typically support row polymorphic effects (fancy generics) so you can make a function generic over "color", avoiding the "function coloring" problem.
Now, having said that, why did I say I agree with you? Well because algebraic effects are a lousy user-facing feature. You almost never want to implement your own handlers, at best you'll plug in a custom handler for the IO effect and that's about it. (And for exceptions of course, but that's just exceptions with extra steps)
Where they shine is for the language implementer. They provide a framework on which exceptions, generators, async/await and even prolog-like backtracking can be implemented, while (very importantly) defining how they compose. That's really the bit that makes them so interesting from a research point of view and why they might make it into the mainstream languages, even if the language doesn't actually ever expose them for you to use.
You might like my capability-based effect system for Haskell, Bluefin[1], then. If a Bluefin effectful function throws you can see it in the type system. If you want to have the capability to throw, you need to pass in an argument of type Throw. For example here "workWithThrow" can only throw an exception because it is passed the Throw capability.
[1] https://hackage.haskell.org/package/bluefinTo be clear, you still pay indirection cost: when you do opt in you have to hope the upstairs implementation is compliant to the contract. But that does also apply to interfaces/typeclasses.