This introduction to effect systems, specifically the first part, may help you understand: https://www.youtube.com/watch?v=lRU6TDgadqw
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.
- “I need mocking and this requires the effect system” – no, seen the
- 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, understandingReader
, andState
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.
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.
…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
-
Granularity (effect systems can provide more precise types than
IO
since the latter is essentiallyMonadEverything
). -
Dynamic dispatch.
to be differences?
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.
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:
-
Browse and search packages | Hackage (
effect system (deprecated:false)
)
there is over 250 packages pertaining to effect systems. As for exception “frameworks” :
-
Browse and search packages | Hackage (
exception (deprecated:false)
)
…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?
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?
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.
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:
-
Let’s start with versioning, because the issues are pretty easy to see there. Let’s say I create a method foo that declares it throws exceptions A, B, and C. In version two of foo, I want to add a bunch of features, and now foo might throw exception D. It is a breaking change for me to add D to the throws clause of that method, because existing caller of that method will almost certainly not handle that exception.
Adding a new exception to a throws clause in a new version breaks client code. […]
-
The scalability issue is somewhat related to the versionability issue. In the small, checked exceptions are very enticing. With a little example, you can show that you’ve actually checked that you caught the FileNotFoundException, and isn’t that great? Well, that’s fine when you’re just calling one API. The trouble begins when you start building big systems where you’re talking to four or five different subsystems. Each subsystem throws four to ten exceptions. Now, each time you walk up the ladder of aggregation, you have this exponential hierarchy below you of exceptions you have to deal with. You end up having to declare 40 exceptions that you might throw. And once you aggregate that with another subsystem you’ve got 80 exceptions in your throws clause. It just balloons out of control.
In the large, checked exceptions become such an irritation that people completely circumvent the feature. […]
-
So how many more programming languages will have to avoid them before people are convinced that checked exceptions are a bad idea?
class Monad m => MonadWhatever m where liftWhatever :: Free (Coyoneda WhateverAPICall) a -> m a
New Haskellers are having enough problems with the monadic interface:
Still, today, over 25 years after the introduction of the concept of monads to the world of functional programming, beginning functional programmers struggle to grasp the concept of monads. This struggle is exemplified by the numerous blog posts about the effort of trying to learn about monads. From our own experience we notice that even at university level, bachelor level students often struggle to comprehend monads and consistently score poorly on monad-related exam questions.
…do they really need more problems by “giving” them extra artefacts from one of the most abstract branches of mathematics?
…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.
There’s also these observations from 2003:
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 more programming languages will have to avoid them before people are convinced that checked exceptions are a bad idea?
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.
the Coyoneda stuff is an implementation detail that end users don’t need to interact with.
That would be a refreshing change:
https://wiki.haskell.org/Monad_tutorials_timeline
I don’t think whatever was half-baked into Java and C++ in the 1990s should forever cast a shadow on that idea.
That looks suspiciously like another variant of the true Scotsman fallacy - “a properly-designed effect system/exception framework shall not cause any problems…”
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.
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.
Even more so, many of your points apply to
Bluefin
the same way as they apply to records of functions.
I re-read my points and can’t find which of them apply to a record of functions.
I think what makes Haskell unattractive to newbies is the lack of simple guidelines on how to structure applications.
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.
Meanwhile, I still see
mtl
recommendations, I guess for the lack of better “standard”.
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”
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.
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.