Why use an effect system?

If you know what the effect is you can react accordingly.

You can retry if there’s a connection timeout but you can choose not to get stuck retrying if the file you want to send is corrupted.

You can use custom exceptions here but now you have to track what comes from where and catch accordingly.

You can argue that all alternatives are abused and turned into an antipattern.

People abuse checked exceptions and now you have 50 different checked exceptions spreading across functions when you could use a result type. Some checked exceptions have no real reason to exist: for example the constructors that take string arguments and throw an exception when the argument is unsupported when all you needed was an enum.

People abuse unchecked exceptions and now you don’t know what or where can explode for whatever reason so you add a top level try catch and have to debug on production and add try catches on the classes you find out you need granularity and you better wish reality doesn’t change otherwise it’s patch time again. Turns out that unreliable webservice does want a nested try catch rather than blowing up and trying again 10 minutes later.

People compromise: now they are writing “throws Exception” everywhere and now you can see what blows up. And everything can blow up. For whatever reason. At least you know now. Maybe it’s an SQLException, there was a timeout because the database got a nasty query. Maybe it’s an IOException because the file was locked for some reason. Maybe it was a null pointer? Who knows. It’s the unchecked exceptions abuse scenario made explicitly manifest. Also, please don’t turn exception messages into stringly typed exceptions.

People ditch exceptions and now use result types for everything. Now there’s question marks absolutely everywhere. Not knowing where, which and how to handle the results, exactly, they panic and call it a day. It will never happen anyways, right? Now they want to make the program runnable long term, reliable, and scalable, so they introduce threads to go blazingly fast and they have to catch the panics to stop the program from crashing. Back to exceptions we go.

I think the universal conclusion we can draw from all these cases is that it’s better to push IO, the allmother of exceptions, to the edge of your program, as far away as possible. Handle them however you like. Discretion is key.

2 Likes

If you know what the effect is you can react accordingly.

But you won’t know if all you’ve got is an I/O action:

  • produced by some effect system as “the lowest common denominator”.

  • and that effect system can’t cope with the failure.

Therefore you still have to track what is failing where and catch accordingly.


You can argue that all alternatives are abused and turned into an antipattern.

There’s no need to argue - according to:

there is over 250 packages pertaining to effect systems. As for exception “frameworks” :

…only 217 (but I note that some are also connected to certain effect systems).

So can any unifying concept be abstracted from all of this?


[…] IO, the allmother […]

type IO a = Eff All a

Prior to version 1.3, Haskell went even further - IO a didn’t exist! So the entire program was just an ordinary effect-free function:

main :: [Response] -> [Request]

with the role of runDialogue (from the aforementioned dialogue package) delegated to a rather-hefty subroutine in each Haskell 1.2 implementation. But this denotative style was deemed embarrassing, and prompted the switch to monadic I/O in Haskell 1.3:

But now IO a is deemed “semantically opaque”, and apparently needs to be finely-sliced into systems of effects: 250 of them and counting (…unless the effect system doesn’t have a typed effect which matches the actual effect; then it’s back to that “lowest common denominator”, IO a - hrm).

Be they absent from or present in Haskell, effects (side or otherwise) are frustrating for many to to deal with…

I’m not sure the hackage list for effects related packages is a real argument for anything. People like reinventing wheels, especially in Haskell. There are tons of variations for all sorts of concepts: effects, transformers, exceptions, lenses, database libraries, array representations, sorting algorithms, “best way to represent a piece of text”, streaming, testing, you name it.

In the end a handful become big. Then you can have discussions about which library has which good idea. That’s what this thread was originally. It looks like the discussion now is going more towards “effect systems suck altogether”, with wordings like “solution looking for a problem”, comparisons to Java and making Haskell unattractive for newbies.

Maybe we can take a step back. What are we really discussing here? Is it whether one does or does not prefer using effects themselves, or is it whether other people should or should not be working with effects?

5 Likes

The topic of the merits of effect systems is very interesting but has diverged significantly from the original topic. Perhaps a mod would be able to split the recent posts that are not about Bluefin and effectful into a new thread?

8 Likes

You made a lot of good points, but this one got me thinking. Do you propose record of functions as a good alternative to effect systems?

But if that is the case, Bluefin actually is a fancy record of functions solution!

write :: Connection -> Foo -> IO ()
write :: (e :> es) => Connection e -> Foo -> Eff es ()

I don’t understand how one write is modular and reusable, while the other isn’t. Both accept arguments that are going to be used to perform some action. Both exist in codebases where lots of stuff is in IO or Eff respectively. The only difference I see is that IO allows running tons of code, while Eff is restrictive to what record Connection allows.

Even more so, many of your points apply to Bluefin the same way as they apply to records of functions. How come one is closer to AbstractEffectFactory than the other?

I think what makes Haskell unattractive to newbies is the lack of simple guidelines on how to structure applications. Haskell Spring could use the fanciest effect library. It would be fine, as long as it takes you by the hand, like Java Springs does.

Meanwhile, I still see mtl recommendations, I guess for the lack of better “standard”.

Java is great at having good ideas and making them look bad. It made a whole generation of developers fear “abstractions”.

Java doesn’t have IO type, but they do use a lot of some sort of record of function passing to relevant objects, but usually automated with annotations.

5 Likes

I like this idiom propsed by Ollie Charles (@ocharles your SSL cert needs renewing btw).

class Monad m => MonadWhatever m where
  liftWhatever :: Free (Coyoneda WhateverAPICall) a -> m a 

Unless you need to take in monadic arguments for some reason, it’s quite flexible and lets other effect libraries provide the necessary instances without you having to depend on every effect library in the library’s core package.

No, checked exceptions are a bad idea:

  • As mentioned earlier, from The C++ Programming Language, 4th edition (2011):

  • There’s also these observations from 2003:

So how many more programming languages will have to avoid them before people are convinced that checked exceptions are a bad idea?


New Haskellers are having enough problems with the monadic interface:

…do they really need more problems by “giving” them extra artefacts from one of the most abstract branches of mathematics?

Ollie’s post shows the rest of the interface; the Coyoneda stuff is an implementation detail that end users don’t need to interact with.

I do actually mention this article here. Although I focus on different aspects of it, because some are not very convincing.

Versioning one just smells to me like a mix of magical thinking and java-esque obsession with breaking changes. Not a great mix, tbh. Funny how adding D to foo “breaks client code” (it breaks compilation), but not adding D and throwing it anyway, breaks somebody code at runtime, but that is ok.

Scalability also seems like a problem with something else. So a subsystem throws 4-10 checked exceptions, but why not one? Maybe ADT Subsystem1Errors. Oh, sorry, no ADT in Java. Let’s do subclass of subsystem1, catch some of there, and rethrow less. Can’t be done. Oh, well.

So how many more programming languages, after PHP, Python, and JavaScript, have to become stunning successes before we convince ourselves that type systems are a bad idea? Oh, it is not 2003 anymore, and all of those have some form of type checking.

I don’t think whatever was half-baked into Java and C++ in the 90 should forever cast a shadow on that idea.

For one, checked exceptions are provided by libraries here, so no need to worry that standard library readfile will annoy everybody forever.

The other thing is, in Haskell we do a lot of result types, especially in pure code, and many of those are checked exceptions in all but name.

That would be a refreshing change:

https://wiki.haskell.org/Monad_tutorials_timeline


That looks suspiciously like another variant of the true Scotsman fallacy - “a properly-designed effect system/exception framework shall not cause any problems…”

The first one can be used everywhere. It has no dependencies. Its arguments are monomorphic. I can just insert it into any IO computation and it will work without the need to change interfaces.

The second one is tightly coupled to the effect system. It’s impossible to convert to a pure IO action and pass to a non-Eff subsystem as it was specifically designed not to allow Connection e to escape. It’s polymorphic, so any data structure or function that uses Connection e becomes polymorphic despite no real polymorphism is involved. It’s only reusable within Eff framework and unusable outside. Doesn’t look too modular.

I re-read my points and can’t find which of them apply to a record of functions.

Not sure about “how to structure applications” – it very much depends on the application. But some guidelines about good and bad practices are definitely possible. I said a similar thing before: Haskell lacks a body of industrial usage wisdom.

And that’s what frustrates me. Effect systems are highly experimental, most of them are not production ready and/or have very serious flaws. Yet somehow they became a necessary pre-requisite for writing basic programs. They’re not.

If a new experimental thing appears, it’s probably better to advertise it as “look, what an interesting way to program” than “this is the future of Haskell”

2 Likes

Agreed. The more pure code and the less IO the better.

If one wants better control, it should be a discreet decision, not a jump on a bandwagon.

Surprisingly, the most obvious option – providing a pure `IO’ interface – is missing. This would be the easiest for library users to integrate into whatever framework they’re using.

1 Like
  • An effect implementation doesn’t necessarily directly interface with IO at all
  • Limiting the surface area of where IO is used (e.g., only in the final unwrapping of an effect system) has its own benefits which are basically the arguments in favour of using pure functions where possible

Yes, everything must eventually be mashed together into an I/O action. But that’s all Haskell code - it must eventually get to main. That doesn’t stop us from writing pure functions.

1 Like

An effect implementation doesn’t necessarily directly interface with IO at all

That’s why I’ve been taking care to specify the effects as being “external” (as in externally-visible) - as mentioned elsewhere, “internal” effects can be confined runST-style.


Limiting the surface area of where IO is used (e.g., only in the final unwrapping of an effect system) has its own benefits which are basically the arguments in favour of using pure functions where possible.

No.

For a time it was proclaimed that the solution to [chemical] pollution was dilution, which is false. Now for Haskell, a similar proclamation is being made - "the solution to the pollution of effects is the dilution of IO a into individually-typed effects". But one effect seems to always be ignored - the effect on code.

Be it:

  • Eff [... {- "external" effects -} ...] a
  • or regular IO a

…if a change to some obscure definition deep in the program means that definition then relies on an “external” effect, then everything that directly or indirectly relies on that formerly-obscure definition (i.e. its reverse dependencies) that was ordinary effect-free Haskell code must also be changed. That only using the maximum “dilution” - one “external” effect - or potentially all of them is irrelevant: as (correctly) noted in Kleidukos’s presentation, avoiding nondeterminism means all effects must be used in a sequential context which can only be accessed in full via the monadic interface.

So using individual “external” effects is definitely not the same as just using ordinary effect-free Haskell definitions.

The unifying concept is indeed the existence of IO. You can choose how you want to deal with it. Maybe you feel more at ease with a “no missile launches here” tag effect reminded to you by the type system. Maybe not. Thus, each library with its opinion.

Also, from the paper:

The representation was error prone.

It certainly was! So when a way was found to do it, dialogue-based I/O (which kept the management of “external” effects outside Haskell) was replaced with functor applicative arrow comonad monad-based I/O in the form of the abstract type IO a (which brought the management of “external” effects inside Haskell).

But now, IO a has been deemed as being semantically error-prone, in need of dilution into separate “external” effects. However there’s a problem here too - as noted in Kleidukos’s presentation, side effects are arbitrary (with “external” effects being externally-visible side effects). So how IO a should be diluted is also arbitrary - there are no “unit effects” in the same way there are chemical elements…and now there are more effect systems for Haskell than the 92 naturally-occurring elements of chemistry.

To summarise:

“External” effects are managed… Problem/s
outside Haskell (“wrapper” ) Error-prone (finnicky!)
inside Haskell (IO a) Error-prone (semantically!)
inside Haskell (effect system) Error-prone (wrong choice!)

…are there any other alternatives that can work for Haskell?

In my view the only true benefit to effect systems is that it’s the only sane way of getting dynamic dispatch in Haskell, so for anything production-grade you *could* have a mirror test system that behaves exactly the same, but calls test versions of all real-world things it links to. It’s still a remarkably hard thing to achieve however: you have to know how to structure your code and you have to avoid any type shenanigans because recursive type families are exponentially slow.

I sympathize with the idea that it would be nice to keep track of which effects are used in any given function, it’s a strongly typed language after all, but having to choose one of five effect libraries and then getting “rewarded” with both a bulkier codebase and performance overheads squarely puts this in the “only use at work on the high level” territory. If GHC had seamless native support for this and some way to precompile functions that only use one implementation at runtime, it would be a no-brainer.

I don’t see a natural benefit of using Readers over plain argument passing. Passing arguments indeed looks bulkier at the first glance, but it allows me to divide the context to the smallest necessary bits at every point. Reader on the other hand necessitates using lens (or more recently field selectors), blurs the line between which arguments are actually needed in a given function, and does not look any prettier.

I don’t see a need in checked exceptions, I lean on the side of “if I expect something to fail without terminating the process, then it’s not an exception”. It’s a natural extension of trying to decouple everything pure from effects, as I can simply use datatypes to convey that an undesired (but not critical) condition has occurred instead of breaking the control flow. It’s also faster.

Regarding lack of modularity in effects libraries, that’s pretty much how all of Haskell’s ecosystem works: instead of providing the minimal tools and letting users stitch things together, it’s instead expected that you use one of fifteen convenient runner functions and for anything beyond that you have to dive into non-PVP-compliant internals. This won’t change unless the community agrees it’s undesirable, good luck with that.

4 Likes

Regardless of whether you reflect the change (throwing a new type of exception) in the type signature or not, throwing a new type of exception is a breaking change (i.e. can break clients), and the users should be aware of it. It should cause a major version bump in your library even without checked exceptions.

Clients then need to consider what to do about the exception, and revisit the call sites.

My observation is that a lot of commentary on checked exceptions from 2000s and earlier (mostly in the context of C++ and Java) do not apply to today’s type systems, scale, and language features. Java did it poorly, and left people traumatized.

There will always be a use case for unchecked (and asynchronous) exceptions, like StackOverflow, HeapOverflow, and ThreadKilled. It probably makes sense to give the users the ability to throw unchecked exceptions as well, and let them decide when it makes sense to have an exception checked vs. unchecked.

Checked exceptions can be modeled as effects, and I suspect with polymorphic variants they can be conveniently composed (I have some notes on this here). No one proved that they can’t be made convenient to use.

1 Like