Why use an effect system?

I think I finally understood this concern. Effect systems (those named in this topic at least) are not about breaking IO into pieces, they are not even about IO at all. They are about built-in/custom effects and restricting functions to specific effects. Of course, there are performance and ergonomics issues too. One of the ergonomic considerations is providing a way to make effects out of the existing code. Which happens to be Haskell code, and a lot of it lives in IO. So those libraries provide an easy way to make effect use IO, to use IO functions.

Just like effectful provides an easy way to integrate mtl code to make effects. Nothing profound about that at all.

1 Like

Unfortunately effects aren’t always so easily restricted:

Why does this matter?


The latest entry to Philip Wadler’s list of notable quotes:

Now another era is being foisted upon Haskell, an era where someone might have to generate an I/O action out of a library using fused-effects and provide it to a library using speff. Can anyone seriously argue that this is an improvement?

I don’t get it. Is this a trick question? In effectful the answer is type All = '[IOE]. Why did you ask the question and what’s the consequence now that you have an answer?

Then IO a can be superseded in a future Haskell standard without breaking every Haskell program in existence. (can we assume that there are no more Haskell 1.0-1.2 programs still in use ? :-)

So for example, future Haskellers could use Eff All ... directly in FFI calls:

foreign ... actionWithEffect :: ... -> Eff All ()

(or use a subset of All).

But as I recall…IOE is connected to the current definition of IO a, so you would need to sever that connection somehow to avoid a vacuous cycle between the two.

Sure, just define effectful's Eff directly in terms of RealWorld rather than IO.

You mean the RealWorld mentioned here?



…how exactly should that be explained to future beginners?

How did we get into discussion of changing the definition of IO (which is obviously not going to happen)? Why would it be relevant to the topic?

10 Likes

I will preface this that my understanding through reading this thread is that effect systems seem better for doing things with IO; therefore my pure logging endeavours below might not put them in the best light but I’d like to share what I’ve learned so far.

===================================================

I’m currently making Snake in Haskell via Brick as a learning opportunity and decided I wanted to learn about effect systems and see what it was like to combine effect systems together with mtl/transformer-style. To understand 1) Can I make a logging system with a known name? and 2) if I can, how does it work alongside Brick?

Brick’s EventM type is a big ReaderT of stuff and I wanted to do logging on the side but without adding to the overall stack of Monad Transformers.

I choose Bluefin as the one to learn and came up with this for my logging solution.

My initial plan was to have the whole Eff es Eventlist as a record in my overall GameplayState type because then later, I can call runPureEff and extract the Eventlist out of it. I couldn’t get this work by adding Eff e Eventlist to the GameplayState type as then it becomes GameplayState e and I struggled to get logInGame function to be happy when called in my eventHandler function (some type stuff, couldn’t figure it out why it wasn’t happy).

So right now I just build the list at the time of usage.

I thread it into my main program like so:

runLogger :: (forall e. Logger g e -> Eff (e :& es) r) -> g -> Eff es EventList
runLogger f gps =
  execWriter $ \writ -> do
    runReader gps $ \rea -> do
      useImplIn f $ Logger (mapHandle writ) (mapHandle rea)
...
handleMovement :: (e1 :> es) => ([a1] -> Either a2 (K.KeyDispatcher KeyEvent m)) -> BrickEvent n e2 -> Logger GameplayState e1 -> Eff es ()
handleMovement disp ev (Logger writ readstate) = do
  gameplaystate <- ask readstate
  let logaction = getKeyEvent disp altConfig ev
      tick = gameplaystate ^. tickNo
  addToLog writ tick logaction
...
logInGame :: BrickEvent n e2 -> GameplayState -> EventList
logInGame ev gs = runPureEff $ runLogger (handleMovement gameplayDispatcher ev) $ gs
...
eventHandler :: BrickEvent MenuOptions Tick -> EventM MenuOptions GameplayState ()
eventHandler  ev = do
  glf <- use gameLog
  gps <- get
  case gs of
...
    Playing _ -> do
      zoom gameState $ handleGameplayEvent' ev
      gameLog .= (logInGame ev gps <> glf)
      tickNo %= (+1)
...

However, for me this solution feels like cheating because at the end of the day, all my Logger es is doing is generating a singleton event to append onto the front of my EventList which is a linked list and I’m just appending them onto the front of the list by calling a function ==> a lot of work for (++) - which really should become a (:) in this current state - and function to pattern match on KeyEvents. Still, a lot has been learned so far.

Ideally, it would be nice to get the Logger e or the Eff e into the GameplayState type but I can’t figure it out right now.

I’m still experimenting around and trying to understand it and I have nowhere near the capability as anyone used to working on this stuff on a regular basis :’) so apologies if I have butchered anything.

4 Likes

The impressions I acquired from the majority of other’s posts on this topic was that:

  • IO a is now more hindrance than help.

  • the use of a “piecemeal” monadic type for effects have been hindered by adverse attitudes towards earlier (and allegedly-defective) implementations (with frequent criticism of Java’s checked-exception mechanism) - apparently it can be done correctly in Haskell.

So I proposed this challenge:

type IO a = Eff All a  -- what is "All"?

…with a view to a future Haskell standard - if IO a could be defined as a special case of a more general system of effects, doing this would mean moving closer to having one standard system of effects that everyone can improve together, as an alternative to everyone continuing to build their own.

But here’s something else I’ve just realised - by being able to switch between IO a's current definition and that special-case definition right now (for example by reusing some of the alternate Haskell preludes) it would be possible for the authors of effect systems to more-or-less use the entire Haskell ecosystem to test and refine their offerings.


As I have now mentioned a few times already, a more permanent change for the management of effects would be a matter for a future Haskell standard. But it will be one of many matters that the writers of that standard will have to contend with; this being another:

Hence the increasing relevance of this edited comment:

Yes, this is exactly the case currently.

I wouldn’t explain it to beginners. It’s an abstract type. Its internals are irrelevant to most people, including beginners. I’m not an educator, so maybe I’m missing something, but I don’t understand the need some people feel to “explain” IO (for example, by saying it’s a “pure program which produces an impure program for the RTS to execute”).

3 Likes

If the intent is for multiple libraries to share Eff on the type level, what would the term-level definition for Eff be? Also I don’t know how you’d prove All is the same as “all effects in scope” with current Haskell type-level features.

I can definitely imagine a future version of GHC that ships an effect system as a boot library plus an extension that switches between presenting all effects through Eff or through IO (for beginners), but that’s about it.

…so presumably that problem has already been solved.

Yes, that is my claim: IO-based effect systems such as Bluefin and effectful solve the “Haskell effect systems” problem.

2 Likes

No: IO a-based effect systems such as Bluefin and effectful can solve the “Haskell effect systems” problem once they are independent of IO a, to the point of being able to use Eff [...] a (with “external” effects) in FFI calls.

So is this the case now, or isn’t it?

A function without any effect is called total and corresponds to mathematically total functions – a good place to be. Then we have effects for partial functions that can raise exceptions (exn), and potentially non-terminating functions as div (divergent). The combination of exn and div is called pure as that corresponds to Haskell’s notion of purity. On top of that we find mutability (as st) up to full non-deterministic side effects in io.

The Koka Programming Language

1 Like

So can those full non-deterministic side effects in io be encoded in eff, to the point of being able to define:

main :: Eff All ()

instead of:

main :: IO ()

…in Haskell?

No idea, that’s above my level.

1 Like

Well, both questions were intended for @tomjaguarpaw’s attention - but thank you anyway.

About mocking with records-of-functions:

Passing records of functions makes you responsible for juggling all that as extra arguments instead of constraints. It’ll definitely work, the question is just how much inline visual clutter the team you’re working in is willing to endure.

In modern OOP programming (which the record-of-functions approach closely resembles) most of the administrative tedium of passing parameters for component wiring is handled by type-directed dependency injection frameworks, which also exist in Haskell.

One might object that having to learn the workings of a DI library is as much of a burden as having to learn the workings of an effect system. But one key difference I think is that (good) DI libraries are not intrusive, they don’t require the “wired” components to know about the library. And they are only used in isolated points of your application, close to the top level.

But I suspect even DI frameworks might be overkill for simpler applications and you can get by by wiring things manually at the beginning of your application.

4 Likes

I don’t think the “why” has been answered in this thread, the subject seems to be very much a matter of opinion.

Are there good learning resources to learn about these techniques in a gradual fashion?

I can understand why imposing a constraint on the effect at the type level could give some nice garantees: security (there’s no network access in this part of the code), readability (the db-related bug can’t be in this part of the code), etc. But I also feel sympathetic to the view of keeping things simple.

If I understand things correctly, there are roughly five ways to organize one’s app (oldest/easy to newest/difficult)

  1. handle pattern
  2. mtl
  3. transformers
  4. free monad
  5. effect system

I own a few books (which I still have to read) and they pretty much talk about transformers and that’s it.

I understand effect systems are newer, but I’d be nice to have an author spell out and demonstrate the trade-offs of each of these techniques.

6 Likes