4 ms·
Algebraic Effects for the Rest of Us
- HeyImAlex 5mo agoDo effect systems actually avoid colored functions? Don’t most typed effect systems require the used effects in the signature?
- codebje 5mo agoWhen you need to use an effect, you need it in the type. If you directly call a function using some other effect, it propagates into your function. So far, so colourful. 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.
- mrkeen 5mo agoNo, they are function colouring. That's the point. 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.
- captaincrowbar 5mo ago‘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.
- mrkeen 5mo agoDon't declare it as non-logging.
- CuriousSkeptic 5mo agoYou can’t is an acceptable answer. The entitle point of such a feature is to prevent people from doing that.
- tome 5mo agoNot sure why people are saying "you can't" when it seems to me the whole point of algebraic effects that you can. You can define g so that it has no ability to do "general IO", all it can do is yield log messages. Then f can call g in a way that turns the log messages into writes to stdout. For example, here's how you would do it in Bluefin: type Log = Yield String -- workWithLogging cannot do arbitrary IO! -- All it can do is yield log messages, which -- must be processed elsewhere. workWithLogging :: (e1 :> es) => Log e1 -> Int -> Int -> Eff es Int workWithLogging l x y = do yield l ("x was " <> show x) yield l ("y was " <> show y) let result = x + y yield l ("result was " <> show result) pure result -- ghci> example -- x was 5 -- y was 7 -- result was 12 -- 12 example :: IO Int example = runEff $ \io -> do -- forEach determines how each log message -- should be handled. forEach (\l-> workWithLogging l 5 7) (\logMsg -> effIO io (putStrLn logMsg))
- brabel 5mo agoYes, this article is doing a bad job at explaining why you would want effects, and one of the main advantages is exactly that it becomes part of the type system, essentially coloring every single function with a set of effects it needs to be called. As the article used JavaScript it shows what untyped effects would look like, which in my opinion is awful. If you want to use algebraic effects today, I highly recommend Unison. If you’re on the JVM, Flix is doing major advances with effects! https://www.unison-lang.org/ https://www.unison-lang.org/ https://flix.dev/ https://flix.dev/
- satvikpendem 5mo agoOh neat, I'm in the second chance pool. 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.
- epolanski 5mo agoEverything he lists is solved by effect-ts [1] bar, obviously, the language support (effect has its own fiber-based runtime like ZIO's scala). 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/ https://effect.website/
- satvikpendem 5mo agoEffect is pretty nice, I'm not sure how worth it it is for the frontend, but I've heard good things on the backend, but sadly I don't use TypeScript for backend work, mainly Rust, and would love to see something like that there. I'm not sure how much Rust's type system would make it possible though however. 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?
- epolanski 5mo agoIt does tend to naturally bubble upwards as you point out, but you can decide where to stop. 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.
- Trung0246 5mo agoFor some funsie here's my fully working delimited continuation in C with effect handler example: https://godbolt.org/z/3ehehvo6E https://godbolt.org/z/3ehehvo6E No ASM involved so technically portable (although it depends on built-in). Flix equivalent (copy paste to https://play.flix.dev/ https://play.flix.dev/): eff Pick { def pick(): Int32 } def body(): (Int32, Int32, Int32) \ Pick = { let a = Pick.pick(); let b = Pick.pick(); let c = Pick.pick(); (a, b, c) } def handlePick(f: Unit -> a \ ef): List[a] \ ef - Pick = run { f() :: Nil } with handler Pick { def pick(_, resume) = resume(1) ::: resume(2) ::: resume(3) } def main(): Unit \ IO = println(handlePick(body))
- tome 5mo agoI was completely baffled by "algebraic effects" for years. They looked far too confusing for me to want to spend my time on them, and took the "Don’t feel like you have to [get curious about them]" approach. 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 https://news.ycombinator.com/item?id=48334067 [1] https://hackage.haskell.org/package/bluefin https://hackage.haskell.org/package/bluefin [2] https://github.com/tomjaguarpaw/bluefin/issues/new https://github.com/tomjaguarpaw/bluefin/issues/new
- jacobp100 5mo agoContinuations (“function calls”) reduce the amount of optimisations you can do around memory - both space wise and computationally
- tome 5mo agoNot sure I follow what you're trying to say. Can you elaborate?
- sirwhinesalot 5mo agoEquating "algebraic effects" with "continuations" is like saying "if" is just "goto" (which isn't even true, e.g., an if can turn into a cmov or whatever). 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.
- KolmogorovComp 5mo agoI’ve used effects in scala3 with cats-effects and haven’t been impressed. All in all, the way it was used was just to reimplement a very similar interface to exceptions.
- noelwelsh 5mo agoCats Effect is monadic effects. What is discussed here is sometimes called "direct-style" effects, and is an alternative representation. 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.
- Epa095 5mo agoPreviously discussed (with pretty decent comments) https://news.ycombinator.com/item?id=20496043 https://news.ycombinator.com/item?id=20496043
- bazoom42 5mo agoSo this looks like dynamically scoped callbacks. Instead of passing callbacks along as parameters they are declared as “handlers”, and any function down the call stack can invoke them. Is this a correct understanding?
- savq 5mo agoYes. dynamically scoped, and statically typed.
- bazoom42 5mo agoHow can it be statically typed in JavaScript?
- savq 5mo agoAn effect system is an extension of a type system where a function type encodes inputs, outputs and effects. The article mentions that they handwaved all the typing stuff (when you do that, effect handlers are more like delimited continuations). But the types are important. If a function doesn't tell you it's effects, how do you know you have to handle them? You'd have to read the whole call graph. That's why exceptions suck.
- AgentME 5mo agoThat's what I was thinking. You could get almost all of this pretty directly in Javascript by putting a callback function in an AsyncLocalStorage instance or, in other languages, in a thread local variable.
- AgentME 5mo agoI was thinking this through more and realized that a callback directly in a thread local variable isn't quite enough: You need to keep a stack in the thread local variable. Every try-handle block pushes a callback in and pops it when exited. (Javascript's AsyncLocalStorage is naturally nestable so you don't need to manage a stack yourself with it.)
- u1hcw9nx 5mo agoStandard feature of Common Lisp condition system (over 30 years old). https://lisp-docs.github.io/cl-language-reference/chap-9/j-b-condition-system-concepts https://lisp-docs.github.io/cl-language-reference/chap-9/j-b...
- brabel 5mo agoThe CL condition system always gets brought up when people unfamiliar with effects see effects for the first time (example: https://news.ycombinator.com/item?id=38813484 https://news.ycombinator.com/item?id=38813484, another example: https://lobste.rs/s/12m2f0/algebraic_effects_another_mistake https://lobste.rs/s/12m2f0/algebraic_effects_another_mistake). 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 https://news.ycombinator.com/item?id=44078743
- u1hcw9nx 5mo agoCL conditions do what you actually need if you program. CL gives you deterministic state, safe resource management etc. 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.
- oisdk 5mo agoIt’s pretty annoying to comment “this thing was already present in CL 30 years ago”, then to have someone correct you (pointing out that this is a common misconception, and algebraic effects are not equivalent to conditions at all), and then to respond not to admit you’re wrong but instead to say “algebraic effects are bad anyway because you shouldn’t want one of the extra features they give you (nondeterminism)”. By the way, nondeterminism is not the only difference between the two.
- sebstefan 5mo agoI swear I'm not trying to be inflamatory, but this is the _worst_ programming language feature I could ever imagine. I'm not trying to be hyperbolic, if I try to reason about it there is nothing I can come up with that I would dislike more in the realm of recent features that have been pitched in the PL community 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
- movpasd 5mo agoBecause you must statically declare dependency on an effect, it's opt-in. To 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.
- tome 5mo ago> 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 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. workWithThrow :: (e1 :> es) => Throw String e1 -> Int -> Int -> Eff es Int workWithThrow t x y = do let result = x + y when (result > 10) $ do throw t "Too big" pure result -- ghci> example -- Left "Too big" example :: Either String Int example = runPureEff $ try $ \t -> do workWithThrow t 5 7 [1] https://hackage.haskell.org/package/bluefin https://hackage.haskell.org/package/bluefin
- flowerlad 5mo agoThe article discusses algebraic effects but React is mentioned. Don’t make the mistake of saying thinking React is functional. See https://mckoder.medium.com/why-react-is-not-functional-b1ed12a41323 https://mckoder.medium.com/why-react-is-not-functional-b1ed1...
- movpasd 5mo agoOne of the things I find most exciting about effect systems is that they solve some of the issues with typeclasses and traits. The issue with these is you can provide exactly one implementation for a type, and this implementation then applies to your entire program. (The underlying theoretical issue, I guess, is coherence.) This means that code cannot request injection of behaviour _except_ through the type of its data. 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!
- Sharlin 5mo ago(2019) 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.
- tome 5mo agoI've never understood what's meant by "algebraic" in that sense, or what this algebra of combining effect is. Could you say more, or maybe share a link to a useful reference?
- matt_kantor 5mo agoI'm no expert, but perhaps https://arxiv.org/abs/2302.01415 https://arxiv.org/abs/2302.01415 (especially section 2.2, "Non-Algebraic Effects") will help.
- tome 5mo agoThanks. It says > 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.
- sodic 5mo agoThis is my favorite one, short and understandable: https://philipnilsson.github.io/Badness10k/posts/2017-05-07-escaping-hell-with-monads.html https://philipnilsson.github.io/Badness10k/posts/2017-05-07-... 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.
- kenanfyi 5mo agoPardon my ignorance, but isn’t this more or less a fancy goto?
- matt_kantor 5mo agoSure, but only in the sense that all structured programming (functions, if/else, loops, etc) is "fancy goto".
- BoiledCabbage 5mo agoI believe the answer is "yes, but..." Yes, but every every control flow statement in programming is more or less a fancy goto. If, do, while, for, try/catch, ...
- gobdovan 5mo ago[dead]
- sheept 5mo agoAs the article notes, effects (at least as described in the article) are already possible in JavaScript using generators, as long as every calling function is a generator, which I assume the author finds problematic because generators are cumbersome. 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/C4TwDgpgBAMg9gcwKIDMUQMbCgXigbylEgC4oByAG0XIBooBbCAZ2YEMEIzngAnASwB2CKAF8AUMWgAxZqnRZc4qFAA+BIuC4U4kQQFoAJv16ZgcXiDpRjvbnyEiJK9YSllyACzaDDlCPoo-P7WQf5k0sHQElJQAPLAnhC88ma4Gu46icnkYpJaUKmKOMpqsIhFwKXqspXV8dkpaGb5kIXNWABKLACulNh4+KXUCGQAbnD8hqXkuhAGtmYWVmSEYSwRUQDaALr0tvYCwrt5Kl4+fgHr5OOT0ypbcI2HjjtkPYIA1oJwAO6C4gkrWgSEEPSYvDYwAgkX8cg6A3q8GQCPqtVRKA+WH4cEEUAAVFB5uDklCYVFmAAKA5QHhHBAASjIAHF5qTzLwADygkmQ6GwliVegTKb0SrdZh9YBbHkQskC+EKaXkKTkHY7AB8BFKGFxPCgusE0KNzHSIH4EEohgyWg8cwWJiWlmstjEAG5SubLdbCSNKQADWXsxxQdamoRQAAk+Fson9DNKKAsUEp-mw6ygcBQBtxxuAzAAdGGGdqVABIL1Wm2kCjeXz+QJRUJRU6tys+qB+wNguXAEOSgBGiywFgt4bx0dj8cTydTEGww45IEz2cNecLthLQ3LAHod1AAOrQDA+KA9ZjQUwYHq8Zg4vHJk+USiZxqhrF9vVQX78RJEhHMKUFYWlWhLEr25JwtSjojpYDIeiozhQO2BKdoglLkAAIriEDkAmQKYoI2K4qhfpMKwHDaHSjhMlArKCOyFicsiQpQCKhhigiEpSlsLEIlsKpaGqmqlshIHWm4toUCM1jkewnB5ARH73qhP6JAAsiAyIII4MD8AOkKWJyAAqRIAB7GoYpqVFqlI6mwz4DmwGCfGQlIljgWr0YxXKmeofFKsKdycUq3H9FsxkCaq6plOKvThQFZhRUJ6oauItHeXyTHGUFortKF8XSpFgmQMJWrbjmgj6pweJ4E+lBOS57nDPOUAMRZ6Q1QW7XAM1Ki-J4LaUgAhD1BaGDhW6lCohr6hACLpGNYwOT0EDTZ2rWmJK-RkHF21FclpUxeoe08YlWCHbh6rrfw2aUvNSoFrEOAvdJNBTSon1QFtUrpLNcD+AWfoPWYBZyZRCZfaIRKUBeolfT9-RmuJ-5KutSEqD1nXzN1EAWZSiPAJDramMAN54ktK1rRIQA https://www.typescriptlang.org/play/?#code/C4TwDgpgBAMg9gcwK...
- satvikpendem 5mo agoLook into Effect TS. https://effect.website/docs https://effect.website/docs
- Findecanor 5mo agoGood article! I've been struggling to grasp what these new "effects" were. 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.
- KolmogorovComp 5mo agoWhile the effect system seems way more general, it’s a bit sad the example only talk about logging or IO, both of which can be simply done with dependency injection
- tome 5mo agoIt's my belief that all effects can be done with dependency injection in some form, at least, I'm not familiar with ones that can't. Even arbitrary delimited continuations can be implemented by injecting a reference to the continuation prompt on the stack.
- Izkata 5mo ago> How Is All of This Relevant to React? > Not that much. You can even say it’s a stretch. No mention of React's built-in Context? Because this is really similar. Not exact for sure, this particular example wouldn't work for a few reasons, but as a comparison this looks to just be a more generic version of that.
- mastermage 5mo agoNice read i got some more conciseness on Effects and the more I learn the better i find them.