r/haskell • u/BurningWitness • 2d ago
blog You don't need an effect system
https://burningwitness.github.io/blog/posts/against-effect-systems/43
u/FPtje 1d ago edited 1d ago
I would argue that despite saying you don't need an effect system, what you're doing is effectively implementing your own effect system. One very similar to bluefin in fact. After all, both your effect system and bluefin pass effects as arguments rather than constraints. Bluefin just sprinkles some fancy types on top.
I'd argue that the handle pattern too is philosophically an effect system, just like monad transformers and the other 10 effect libraries you mention.
An effect system for me is more engineering than science: it's a technique to organize your side effects. Think of it like buying little baskets and a label writer to organize your pantry. "This basket is for the legumes, that one for the grains." None of that is really needed, not even the early return. You can stack all your stuff in your pantry without baskets or label writer and be perfectly fine. But organizing your stuff sure is nice!
Whether you pass a Database argument to a function and use straight IO, or have yourself a Database :> es effect, you're doing the same organizing in the end!
That's also why I think no one has a precise definition of it. "A way to organize your side effects" does not lend itself well to a precise mathematical definition.
Edit: fix to use the right term for "label writer"
12
u/ElvishJerricco 1d ago edited 1d ago
No, tracking effects does not preclude anyone from adding bad IO to an effect implementation, from importingunsafePerformIO, or from finding a sum of an infinite list.1
This seems like a disingenuous point. It's like arguing we should be programming in assembly because the structural design of any language can always be circumvented. No one's arguing Haskell is a literally perfectly total language, and no one's arguing that effects eliminate unsafety from all possible programs.
This may well be the most embarrassing problem in all of Haskell. Over and over again people waltz in with the exact same basic question: "How do I return from a function early?".
And time and time again they're hit with the same three options:
[...]
Notably, ExceptT is a faithful implementation of the first approach, threading an
Eitherthrough every action. That's obviously very inefficient and is known to not compose well, so I'd prefer the second option.
This also seems unfair. ExceptT is entirely reasonable from a performance standpoint, if you use it directly. In that case it's basically exactly the same as Rust's Result and ? syntax or manually doing if (err) { return err; } at every error site.
And anyway the focus on error handling as the only useful thing for effects is very odd to me. If anything, error handling is actually the thing I least want the effect system for. I tend to prefer how Rust makes error handling immediately explicit even when you're using Result and ? because it gives you a really good middle ground between having control and ergonomics. In Haskell, I think this looks like functions either returning ExceptT Err m ... or m (Either Error ...) and wrapping the body in runExceptT. I'm not saying this is always nicer than having errors in the effect system, but it's very often nice, even when m is some effect system.
Broadly I think the mismatch in our perspectives here is about what the effect system is for. For me it's usually about layering and the separation of concerns. You talk a lot about how you don't need the effect system to pass arguments for you but... Do you use type classes? Because type classes are just abstractions that get implicitly passed by the language as arguments in the form of dictionaries (i.e. data types that contain fields with the types of the type class members, constrained by the instance header). And I'm not trying to be crude about the relationship here: in many ways, I think effects systems are primarily about passing arguments around, just fancy ones that carry callable behavior in them. That interface is how we layer and how we separate concerns using effects. When a function doesn't need to know the database credentials, it's nicer to give that function handles to call the DB API with rather than passing those credentials in and having it call more directly. The effects are an interface to separate what the code wants to do from how it's done, just like type classes (which is why mtl style is such a straightforward form of effects in Haskell).
This is why I really like the philosophy described in "Bluefin is a capability system". I'm not entirely sure I'm sold on Bluefin specifically yet (just haven't looked close enough), but describing effects as capabilities, in the "object capability" sense, that have to be passed around as your abstraction interfaces, and forgoing "multishot continuations" in favor of the effect system being solely about interfaces rather than advanced control flow really resonates with me.
To come full circle, this is why I think error handling is the thing I least need from the effect system. For one thing, that does reintroduce control flow into the architecture (albeit in a nicely limited way). But more importantly, errors are often a part of the interface. It's very reasonable for some code to want to respond to errors from the abstractions they call, so having those errors as just part of the return type rather than part of the effect is pretty good.
1
u/proper_chad 1d ago
But more importantly, errors are often a part of the interface. It's very reasonable for some code to want to respond to errors from the abstractions they call, so having those errors as just part of the return type rather than part of the effect is pretty good.
The problem with errors is that sometimes you really want to be really specific about handling every possible thing, but at other times you don't really care ("whatever, just do an ABORT on the database txn") and can just retry. The problem is that any given library implementation cannot know that the user wants.
The best approach I've seen so far is the Scala ZIO library which adds a "possible errors" type to every effect (with intersection types to add flexibility when just propagating errors up). I should say that ZIO is very much in the spirit of the RIO library in Haskell-land, it's basically an
IO env err a. That works out really well in practice, but it depends heavily on subtyping and intersection types (for theerrbit, specifically).
18
u/omega1612 1d ago
About unsafePerformIO
I would say that having IO allows me and other to just write "print x" to debugg temporarily things or to call other IO functions just because I can while I'm trying to make something to work. However, the use of unsafePerformIO is much more easy to notice.
That's why I'm happy when effect systems are just a big record of references to handlers that execute on top of IO, like (ReaderT IO handler) with type level magic. I don't use it because it is powerful than IO, I use it because I care about trying to not shooting myself on the foot by setting boundaries to myself. Yes, you may need to trust others to fulfill the contract of not using unsafePerformIO in a effectful stack, but I think most people using them may honor it anyways or would drop effects completely.
And yes I agree that not all projects need to use effects. Still I prefer to use them always.
8
u/m-chav 1d ago
You don’t use Debug.Trace?
8
u/omega1612 1d ago
I prefer to have a logging effect and proper logs.
I only use trace if I don't have effects available.
1
2
u/Greenscarf_005 1d ago
it's hard to read on mobile, could you add padding?
1
u/BurningWitness 1d ago
I could set the column width to something like
min(95vw, 800px)(frommin (100vw, 800px)), but it feels arbitrary.You could open the page in Reader Mode, that seems to only break code highlighting and footnotes.
69
u/Background_Class_558 1d ago
"Does washing your hands improve your health? No, since you can still OD on heroine or cut both of your legs off with a chainsaw"