Why use an effect system?

This introduction to effect systems, specifically the first part, may help you understand: https://www.youtube.com/watch?v=lRU6TDgadqw

1 Like

For me it has a lot of downsides:

  • massive and noisy type declarations
  • repeated noisy declarations – constraints are frequently the same or similar everywhere, so I just skip them when reading the code defeating their purpose
  • unnecessary with good function names:
    readFile :: ... -- even without the type I can guess what it does
    
  • of no help with bad names:
    createMess :: (MonadNo m, MonadMatter m, MonadWhat m ...
    constructMess :: (You :> es, Put :> es, There :> es) =>
    ensureMessIsProperMessy :: (e :> es) => It e -> Doesn't Help es
    
  • leaks implementation details
  • at the same time hides the implementation (I need to go to the definition of MonadFoo, usually with only one instance to find what is actually done there)
  • unmodular (change in the implementation details requires updating callers)
  • infects the code (Eff everywhere)
  • a vendor lock-in (try to switch the effect system)
  • very experimental and loose popularity quickly
  • tightly coupled and not reusable: “effects” are developed as a part of the system instead of standalone modules with a generic pure or IO interface
    -- this is modular and reusable
    data Connection
    connect :: IO Connection
    write :: Connection -> Foo -> IO ()
    -- this is not
    class MonadConnection m ...
    data Connection eff 
    withConnection :: (forall e ... Eff ...) -> Eff ...
    -- No, these "effects" don't use the well defined 
    -- IO primitives from the above.
    -- Those primitives are implemented right inside 
    -- the "effect interpreter".
    -- I've seen this many times
    
  • frequently developed on false premises:
    • “I need mocking and this requires the effect system” – no, seen the Connection above? It’s an abstract data type, it can be a record of functions and has your mocking.
    • “I need to manage effects, and this requires the effect system” – no, unfortunately it requires thinking, understanding the domain, design, prototyping, a whole lot of software engineering techniques, and a good taste. There’s no royal road to a good software system. You won’t replace all of this with an effects interpreter.
  • a solution looking for the problem
  • rigid. I can do a lot with simply typed pure functions, much less so when everything is noisy-typed
  • complicates programming instead of simplifying
  • evangelized as a way to go, while there are well known simpler ways
  • looks like an AbstractEffectFactory from Java architecture astronauts went to Haskell.
  • looks like an OOP code accessing implicit this. Unlike OOP, related functions are not grouped together, so it also looks like a pile of spaghetti accessing global variables (well tracked global variables indeed).
  • makes Haskell unattractive for newbies. IO/pure separation is already unusual, understanding Reader, and State can take quite some time, and now everyone speaks about effect systems?

Perhaps I should stop there, though I’m sure I can continue the list.

When I first heard about adding a lot of “effect” annotations 15 or so years ago, my colleague pointed out that there was a similar thing in Java – checked exceptions, and they’re widely considered an antipattern.

One of the posts about checked exceptions I found on duckduckgo puts it well:

What do water wings and checked exceptions have in common?
At the beginning you feel safer with them, but later they prevent you from swimming quickly.

1 Like

From The C++ Programming Language, 4th edition:

Exception specifications provide runtime enforcement of which exceptions a function is allowed to throw. They were added at the energetic initiative of people from Sun Microsystems. Exception specifications turned out to be worse than useless for improving code readability, reliability, and performance. They are deprecated (scheduled for future removal) in the 2011 standard. The 2011 standard introduced noexcept as a simpler solution to many of the problems that exception specifications were supposed to address.

Even C++ realized (in 2011!) that pure functions are more powerful and expressive than effect systems.

1 Like

…and before someone complains about comparisons of effects (and their systems) to exceptions (and their specifications) not being “legitimate” :

Since no-one has has provided one, then is no the answer?

Do you not consider

  1. Granularity (effect systems can provide more precise types than IO since the latter is essentially MonadEverything).

  2. Dynamic dispatch.

to be differences?

1 Like
  1. What’s the point of granularity means if all those finely-slided effects are going to be all mashed together again into an I/O action?

  2. Hrm - static versus dynamic dispatch; I thought that was still being clarified here (#9, #12, #14, #15, #16) - what was the conclusion again?

Now to Kleidukos’s suggested presentation - I noticed this from the slides:

…which looks similar to:

in Monads … Get Out Of My Type System. So did that group of developers then “pile onto” the effect-systems “partywagon” ?

Here’s a hint for those who don’t want to watch my suggested presentation:

…and presumably this is one of the “marriage certifications” :

Since that article has been publicly available for over twenty years…I for one would tend to think that the connection between monads and effects was already known by the majority of developers in that group who were frustrated with “type acrobatics” (or “mandatory type-system fighting” ) - so is their presentation about some “new-fangled” effect system which solved all their problems?

You’ll just have to watch it to find out…

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.