tomjaguarpaw/bluefin

MTL interop

Opened this issue · 8 comments

Looks like you can't do seamless (i.e. without newtypes) MTL style effects interop, as already evidenced by how Eff can't have a MonadIO instance?

That's right, you can't give Bluefin's Eff a MonadWhatever instance, because the effect is at the value level rather than the constraint level.

This approach of passing in multiple effects seems not entirely terrible:

exampleMTL ::
(e1 :> es, e2 :> es, e3 :> es) =>
IOE e1 ->
Exception String e2 ->
State Int e3 ->
Eff es r
exampleMTL ioe ex st =
runMTLStyle $
handleMTLWith ioe $
handleMTLWith ex $
handleMTLWith st $ do
setWhateverTo 0
incrementWhatever
incrementWhatever
i <- getWhatever
liftIO (putStrLn $ "Whatever was " ++ show i ++ ". Now I will fail:")
fail "Failed"

Looks like you can't do seamless (i.e. without newtypes) MTL style effects interop

So, for a better answer to your question, you can do MTL style effects interop without (defining your own) newtypes. Whether that's "seamless" or not is up for debate.

Considering that in the "real world" code you will have interleaving of MTL style functions and "native" effects, looks like it's going to be a pain with this approach 🤔

in the "real world" code you will have interleaving of MTL style functions and "native" effects

By '"native" Effects' do you mean Eff? If so, that's fine too (see liftEffStack):

exampleMTL ioe ex st y =
runMTLStyle $
handleMTLWith ioe $
handleMTLWith ex $
handleMTLWith st $ do
setWhateverTo 0
incrementWhatever
incrementWhatever
i <- getWhatever
when (i >= 100) $
liftEffStack (yield y i)
liftIO (putStrLn $ "Whatever was " ++ show i ++ ". Now I will fail:")
fail "Failed"

I'm not going to say this is the most ergonomic thing I've ever seen, but considering it was the first thing I thought of and I worked on it for about an hour, I think it's promising.

You could also apply the conversion to individual actions to get the back into Eff immediately. I don't know what's going to work out the most ergonomically in practice. In this example everything is done in Eff so the yield doesn't need to be lifted in any way.

exampleMTL2 ::
(e1 :> es, e2 :> es, e3 :> es, e4 :> es) =>
IOE e1 ->
Exception String e2 ->
State Int e3 ->
Stream Int e4 ->
Eff es r
exampleMTL2 ioe ex st y = do
mtl (setWhateverTo 0) st
mtl incrementWhatever st
mtl incrementWhatever st
i <- mtl getWhatever st
when (i >= 100) $
yield y i
mtl (liftIO (putStrLn $ "Whatever was " ++ show i ++ ". Now I will fail:")) ioe
mtl (fail "Failed") ex

What if you want to call a function f :: (MonadA m, MonadB m) => m () etc. for more classes?

That one needs the following form (because mtl is just runMTLStyle fused with handleMTLWith):

runMTLStyle $ handleMTLWith aHandle $ handleMTLWith bHandle $ f

Maybe this form, or some infix operator, would be more ergonomic? It remains to be seen.

runMTLStyle (f `handleMTLWith` aHandle `handleMTLWith` bHandle)