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.
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?
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.
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 effectfulare 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 Effreally 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 HaskellIO 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.
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.
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.
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.
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…
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.
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.
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.
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?