re-xyr / cleff

Fast and concise extensible effects
https://hackage.haskell.org/package/cleff
BSD 3-Clause "New" or "Revised" License
105 stars 6 forks source link

effect order dependence when discharging multiple effects #26

Closed goertzenator closed 2 years ago

goertzenator commented 2 years ago

I wrestled with the my top-level run function for a while which boiled down to this:

-- runApp :: Eff '[IOE, Trace] -> IO ()   -- swapped effect order, doesn't compile!
runApp :: Eff '[Trace, IOE] a -> IO a
runApp = runIOE . runTraceStdout

My actual app of course has a bigger list of effects. It was surprising to see order dependence here when most of Cleff usage has order independence, for example:

myFunc1 :: '[IOE, Trace] :>> es => Eff es ()
myFunc1 = do
  trace "hi"
  liftIO $ putStrLn "there"
myFunc2 :: '[Trace, IOE] :>> es => Eff es ()  -- swapped order is okay here.
myFunc2 = do
  trace "hi"
  liftIO $ putStrLn "there"

Not a huge deal; I can easily work around this now that I know about it. But, is there a way to specify the effects for runApp in an order independent manner?

evanrelf commented 2 years ago

You get order independence in the latter two examples because you're using the :> constraint (via the :>> type family).

I think this change should solve your problem:

-runApp :: Eff '[Trace, IOE] a -> IO a
+runApp :: '[Trace, IOE] :>> es => Eff es a -> IO a
 runApp = runIOE . runTraceStdout
evanrelf commented 2 years ago

Also, the correct order in the first example is defined by the order of your run* functions. You need to handle the Trace effect before you can handle the IOE effect, so Trace gets popped off the head of the type-level list first.

In general you should always try to use :> or :>> to avoid requiring a concrete ordering.

goertzenator commented 2 years ago

I think this change should solve your problem:

-runApp :: Eff '[Trace, IOE] a -> IO a
+runApp :: '[Trace, IOE] :>> es => Eff es a -> IO a
 runApp = runIOE . runTraceStdout

That fails with "Couldn't match type β€˜es’ with β€˜'[Trace, IOE]’". To fully discharge an Eff it needs to be brought down to a concrete Eff '[] or Eff '[IOE] before you runPure/runIOE.

evanrelf commented 2 years ago

Oh yeah you're right πŸ€¦β€β™‚οΈ Sorry about that.

For the case where you're handling effects, you do need to know there are no effects remaining. And :>/:>> doesn't do that for you.

re-xyr commented 2 years ago

Yeah, unfortunately we can't have that. I think the reason is basically that Haskell doesn't have type-level Sets (except for typeclasses; but it would suck trying to implement many advanced effects features with typeclasses, unless someday we get proper anonymous implicit arguments like Dotty).