Open kamoii opened 3 years ago
I came up to this solution.
data IOEff e ans = IOEff
{ ioeff :: forall a. Op (IO a) a e ans
}
runIOEff :: Eff (IOEff :* ()) a -> IO a
runIOEff =
runEff . handlerRet pure IOEff{ioeff = operation $ \a k -> pure $ a >>= runEff . k}
I'm not sure if this is the right way. Anyway it runs as expected.
testIOEff :: (IOEff :? e) => Eff e String
testIOEff = do
perform @IOEff ioeff $ putStrLn "hello"
perform @IOEff ioeff $ putStrLn "world"
pure "Hello world"
main :: IO ()
main = do
i <- runIOEff testIOEff
putStrLn i
$ stack run
hello
world
Hello world
Werid behaviour. Added Reader Sting
effect and now it outputs nothing. Not even tha "Hello world"
return value.
testIOEff :: (Reader String :? e, IOEff :? e) => Eff e String
testIOEff = do
name <- perform @(Reader String) ask ()
perform @IOEff ioeff $ putStrLn ("hello " <> name)
pure "Hello world"
main :: IO ()
main = do
i <- runIOEff $ handler (Reader{ask = value "alice"}) $ testIOEff
putStrLn i
$ stack run
Maybe related to #1? stack resolver is nightly-2021-03-10
.
Tested with resolver lts-14.2
, but same result.
@xnning
I glad you're looking into this issue.
Here is a more simpler code to reproduce an issue which I think is related to previous one.
From what I understand, testWrong
and testCorrect
should both return 1
.
testWrong :: Int
testWrong = do
runEff $
handler @(Reader Int) (Reader{ask = operation $ \_ _ -> pure 1}) $ do
i <- handler (Reader{ask = value "foo"}) $ do
perform @(Reader Int) ask ()
pure $ i + 1
testCorrect :: Int
testCorrect = do
runEff $
handler @(Reader Int) (Reader{ask = operation $ \_ _ -> pure 1}) $ do
handler (Reader{ask = value "foo"}) $ do
i <- perform @(Reader Int) ask ()
pure $ i + 1
main :: IO ()
main = do
print testWrong
print testCorrect
$ stack run
2
1
Hi @kamoii, thanks for the bug report. I confirm that the bug in your provided test programs is the same as #1, which has been fixed by the latest commit. We have also updated the library in Hackage and please feel free to try it out.
Regarding running IO, I think essentially the underlying problem is the discrepancy between systems with side-effects as algebraic effect and systems with side-effects as monads. Within the algebraic effects world, we want every side-effect to be modeled as one algebraic effect (in this case, one Eff
). For example, in languages with built-in support for algebraic effects, like Koka, printing works like
fun print : (string) -> console ()
if we map this type into the Eff
framework, we expect an effect
data Console e ans = Console { print :: Op String () e ans }
and
putStrLn :: String -> Eff Console ()
putStrLn str = perform print str
and the effect Console
will be handled by the system.
But in Haskell, we got
putStrLn :: String -> IO ()
and IO
is handled by the system.
Using IO
inside Eff
is very much like wanting to mix up the two different concepts (side-effects as algebraic effects vs side-effects as monads). For now I am not sure if we have a principled way for that.
Another comment is that I think perform
should not pass putStrLn
; instead, perform
should only pass the String to be printed, and it's the handler that will then call putStrLn
to print the string, following the idea that only handlers give semantics to operations. But again, not sure if this can be implemented in a simple and intuitive way.
I completely forgot that there was already a pretty cool (and more importantly: correct) solution in this thread. Let me restate part of my previous (now deleted) comment.
With the above IOEff
we can implement more granular effects that have impure handlers, e.g. for Console
:
performIO :: IOEff :? e -> IO a -> Eff e a
performIO = perform ioeff
console :: IOEff :? e => Eff (Console :* e) a -> Eff e a
console = handler Console { print = function (performIO . putStrLn) }
So, I still think it would be very useful to include IOEff
in this package. Maybe with a warning or clear documentation that it should really only be used to implement more granular effects.
This is also how other libraries do it, e.g. freer-simple
has sendM
and runM
.
By the way, the same approach works for any monad:
newtype MEff m e ans = MEff
{ meff :: forall a. Op (m a) a e ans
}
performM :: MEff m :? e => m a -> Eff e a
performM = perform meff
runMEff :: Monad m => Eff (MEff m :* ()) a -> m a
runMEff =
runEff . handlerRet pure MEff
{ meff = operation $ \a k -> pure $ a >>= runEff . k }
Hi @noughtmare, thanks for joining the discussion.
Yes, such a definition is possible. If we look at the definition of runMEff
(and respectively runM
from freer-simple), the definition simply calls runEff
to escape the algebra effects world, and returns back to the m
monad world (in runMEff
, it further gets back to the algebraic effects world using pure
). This seems to be the trick to switch between the algebraic effects world and the monad world.
Indeed, these definitions may be included, though because of the use of runEff
, the current definition is also quite restrictive, in the sense that it only applies to the case where the monad is the last effect (same for runM
from freer-simple). But maybe this pattern is already useful enough, so that we should provide the implementation for it (especially we may want support for IO
monad).
Last week I opened a pull request that implements this, but let me explain my thoughts a bit more. I think that it is not possible to embed multiple monads in the effect context, because that would require solving the monad composition problem, wouldn't it? So, I think that this is the best we can do. And support for IO is an absolute must in my opinion. Even if you should not use it directly, it is still required to implement more granular effects that can be reinterpreted as IO effects.
I just realized that it should be possible to implement IO with a tail-resumptive handler by passing the realworld token with some local state like this:
{-# LANGUAGE RankNTypes, TypeOperators, MagicHash, UnboxedTuples #-}
module Control.Ev.IO where
import Control.Ev.Eff
import GHC.Types (IO (IO))
import GHC.Prim (State#, RealWorld)
newtype IOEff e ans = IOEff
{ ioeff :: forall a. Op (IO a) a e ans
}
data StateBox = StateBox (State# RealWorld)
runIOEff :: Eff (IOEff :* ()) a -> IO a
runIOEff act = IO $ \s0 ->
let (StateBox s''', y) =
runEff
. localRet (StateBox s0) (\ans s'' -> (s'', ans))
. handlerHide IOEff
{ ioeff = function $ \(IO io) -> do
StateBox s <- localGet
let (# s', x #) = io s
localPut (StateBox s')
pure x
}
$ act
in (# s''', y #)
That could probably be simplified further, but this should already be much faster than the other approach.
That is great -- and also how I would envision integration IO where I/O operations are part of a user defined effect.
Very nice ! We should probably call it handleIO
(instead of runIO
) and the operation performIO
(instead of ioeff
). Maybe shorten IOEff
to IOE
as it may be common)
I feel like storing the realworld token in Local
state which is accessed by using unsafeInlinePrim
is a bit redundant. Maybe an implementation like this works even better:
newtype IOE e ans = IOE { _performIO :: forall a. Op (IO a) a e ans }
performIO :: IOE :? e => IO a -> Eff e a
performIO io = perform _performIO io
{-# INLINE performIO #-}
handleIO :: Eff (IOE :* ()) a -> IO a
handleIO action = runEff $ handlerRet
pure
IOE { _performIO = function $ \io -> Eff $ \_ -> unsafeIO io }
action
{-# INLINE handleIO #-}
But I think unsafeInlinePrim
(and therefore also unsafeIO
) is pretty scary, its documentation mentions that it is the same as accursedUnutterablePerformIO
with the comment:
-- | This \"function\" has a superficial similarity to 'unsafePerformIO' but -- it is in fact a malevolent agent of chaos. It unpicks the seams of reality -- (and the 'IO' monad) so that the normal rules no longer apply. It lulls you -- into thinking it is reasonable, but when you are not looking it stabs you -- in the back and aliases all of your mutable buffers. The carcass of many a -- seasoned Haskell programmer lie strewn at its feet. -- -- Witness the trail of destruction: -- -- * <https://github.com/haskell/bytestring/commit/71c4b438c675aa360c79d79acc9a491e7bbc26e7> -- -- * <https://github.com/haskell/bytestring/commit/210c656390ae617d9ee3b8bcff5c88dd17cef8da> -- -- * <https://ghc.haskell.org/trac/ghc/ticket/3486> -- -- * <https://ghc.haskell.org/trac/ghc/ticket/3487> -- -- * <https://ghc.haskell.org/trac/ghc/ticket/7270> -- -- Do not talk about \"safe\"! You do not know what is safe! -- -- Yield not to its blasphemous call! Flee traveller! Flee or you will be -- corrupted and devoured!
And inlinePerformIO
has the warning:
If you think you know what you are doing, use 'unsafePerformIO'. If you are sure you know what you are doing, use 'unsafeDupablePerformIO'. If you enjoy sharing an address space with a malevolent agent of chaos, try 'accursedUnutterablePerformIO'.
Maybe unsafeDupablePerformIO
is fast enough for our purposes?
Hi, thanks for this interesting library.
Currently, we can only handle effects purely since we can't use IO inside hander. Can we have
IO
-like effect at the bottom of effect stack, and a runner likerunEffIO :: Eff (IOEff) a -> IO a
? (or maybe I'm missing something and this is already possible?).