Why use an effect system?

Hence me writing “a more denotative approach” - unlike Haskell 1.2 and earlier (with no abstract monadic I/O), I/O actions are still being used.

So now that we’ve more-or-less established that effect systems need I/O “black boxes” to handle the (actual) effects of I/O:

Everything that interacts with the real world needs an I/O black box. The real world is a black box (from the perspective of a program).

I’ll rephrase my question:


As both IO ... and Eff ... rely on the monadic interface, is there any other difference between providing:

  • 128 extra exceptions for IO ...
  • 128 extra “external” effects for Eff ...

…?


(and yes: these days, the monadic interface isn’t alone, with the functor, applicative, arrow, comonadic etc interfaces also now available; but I/O in Haskell is still abstract and monadic.)

Good to know that mtl/transformers are not considered a way to go now (though I have nothing against local uses). It took me quite some time to explain the same to fellow team members. Some of them are now converted to a “church of pure functions” and enjoy the simple code without artificial obstacles.

I don’t want to mix them with effects (though I can do it with unsafeInterleaveIO). And yes, lazy values can leak space (though with a bit of experience and a couple of bang patterns it doesn’t happen much), but they make Haskell very expressive, powerful, fast and modular.

Sorry, I meant wait.

For sure. It’s a pretty bad coding style (GOTO) and I don’t want to have any monads here:

  -- simple recursive monadic function will work
  let loop = do
        n' <- get n
        modify total (+ n')
        unless (n' == 0) $ do
          modify n (subtract 1)
          loop
  loop
  -- could be shortened using 'fix'
  fix $ \ loop -> do
    n' <- get n
    modify total (+ n')
    unless (n' == 0) $ do
      modify n (subtract 1)
      loop
  -- but the best in this case would be to have a pure function
  let count 0 total = total
      count n total = count (n-1) (total+n)
  count 5 0

It might work. Unlike the implicit arguments passing approach, superfluous handles really need to be passed in Bluefin. So it’s not like “yes, we use global variables everywhere, but we track them really well”. But the examples I’ve seen so far look like sophisticated solutions to problems that were solved much simpler a long time ago.

That left me open-mouthed for a while. So why did you choose Haskell then?

With another two handfuls of primitives? :slight_smile:

You might be surprised, but all functions you listed are in fact implemented using two primitives: traverse and foldMap (or sequenceA and foldr, depends on Traversable and Foldable instances).

And I much better prefer to just use any pred [a, b, c] than

withEarlyExit $ \ exit -> do
  forM_ [a,b,c] $ \ x -> when (pred x) (exit True)
  pure False

And if I don’t have any I can just write it:

any pred = or . map pred
-- or maybe
any pred = not . null . filter pred
-- or 
any pred = foldr ((||) . pred) False

All of them are much simpler, shorter and faster.

I didn’t mean Bluefin in particular. I mean that IO is well designed, universally supported and that IO experience is transferable from one project to another. Effects systems are all different and have a lot of quirks you have to learn. I especially don’t like how they work with concurrency, a sensitive topic where I don’t want to have any superfluous layers.

1 Like

That’s not the same as Bluefin’s await either.

By “another two handfuls of primitives” do you mean effects? Well, there are only three primitive effects in Bluefin: State, Exception and IOE. I think it’s much easier to understand what for with just a State in scope does than what mapAccumL does.

It doesn’t surprise me. In fact I just explained exactly the same thing! What surprises me is that people want two handfuls of fold combinators when for and for_ generalize all of them.

That looks great, but what if pred or the sequence of elements are effecful? That is not an uncommon occurence.

Well, of course one can write these combinators for Bluefin so that you can write any pred = or . map pred in Bluefin too.

But Bluefin and effectful are just IO, so there’s nothing to learn if you already understand IO. (effectful is actually IO plus a little bit, but really a tiny little bit – and Bluefin’s Eff really just is IO.)

Naturally, if you prefer to program without an effect system you prefer it. I can’t argue with that!

I was talking about the effects systems I see in Haskell. They try to abstract Haskell IO which is a much better abstraction than these effects systems. Haskell IO is open source and this source is usually of a much better quality than that of effect systems.

(a former game developer there) True, there are reference renderers that slowly draw to a 2D array of pixels. But I wouldn’t call this this 2D array an “effect”. It’s a data structure (and a lot of things that lead to have some nice looking bytes in that data structure).

It looks like many standard engineering things – data structures, algorithms, abstract interfaces, data flows, control flows, modules, libraries, components, systems, services, maybe even teams – are all “effects” now. Much like an object in OOP – no one know what it is.

3 Likes

For type safety and composability. IO-based effect systems are as type safe as programming with pure functions (i.e. not using any monad) whilst being more composable, hence I prefer them.

I think the motivation for effect systems is quite simple. If you recognize the utility of distinguishing side-effects at the type-level:


// no side-effects in type signatures

String readFile();

void launchThread(Int);

Int bar(Int,Bool);

void run(); // calls readFile, launchThread, and bar


-- IO to the rescue

readFile :: IO String

launchThread :: Int -> IO ()

bar :: Int -> Bool -> Int

run :: IO ()

Then imo it is easy to see the advantages of distinguishing types of side-effects i.e. increasing granularity:


readFile :: FileReader m => m String

launchThread :: Concurrent m => Int -> m ()

bar :: Int -> Bool -> Int

run :: (Concurrent m, FileReader m) => m ()

Now, where to draw the line is going to be highly personal and likely app-specific. For instance, some apps will care about separating read-only and write-only file-system effects, whereas others will not. And certainly there are downsides to this style of programming (though similar arguments often apply to IO!). But the general idea is a very natural extension of IO.

4 Likes

It’s a similar situation for asynchronous frameworks in Scala:

Monad I Love You Now Get Out Of My Type System (14:18)

…and that’s from 2022 - who knows how many more now exist!

Back to Haskell: suppose your new program requires three libraries larry, curly, and moe:

  • larry uses effectful
  • curly uses speff
  • moe uses fused-effects

…how in Haskell are you meant to make them all work together in your new program?

2 Likes

I don’t see it being a problem. Why would those libraries not expose pure and IO based API in the first place? Or expose both with moe and moe-fused-effects. And even if they did not, with function like readFile :: FileReader m => m String you can always do this.

readFileIO :: IO String
readFileIO = runWhatever readFile
1 Like

One library works with MonadIO and bindings libraries export effects for the underlying implementation.

1 Like

That’s a different question. The question of where to draw the line is about how fine-grained you want your effects to be. Is the pure/IO distinction enough? Maybe it’s too coarse. Conversely, it’s possible to be too fine.

The dimension of “what effect system do we use and are they compatible”, which I think is what you’re getting at with the list of Scala effect types, is orthogonal.

Ultimately, as @jeukshi and @Kleidukos are saying, you can use IO as the lowest common denominator, just like the C ABI is the lowest common denominator for the FFI. If larry exposes an effectful API you can use its run... functions to get an IO API, and then wrap that in whatever effect system interface you like.

3 Likes

It may be a different question, but it arises from the same problem:

  • Each of those async frameworks was written by someone who thought all the other frameworks “drew the line” wrongly;

  • Similarly, each new effect system was also written by someone who thought all the other systems “drew the line” wrongly;

So how many more effects systems will be needed to “draw the line” correctly, with a view to having a standard one for Haskell that can be used by all libraries?


  • So first you finely slice the effects 1002 ways, according to “where the line should have been drawn in the first place” because “you know best” ;

  • only to then mash all those effects back together again within an I/O action!

Then why bother with the hassle of an effect system to begin with? Just use IO a directly, exactly because it is the lowest common denominator that all experienced Haskellers know of - at least then, all debates about which $EFFECT_SYSTEM is “best” can be kept out of code reviews…

1 Like

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…