xnning / EvEff

Efficient Haskell effect handlers based on evidence translation.
BSD 3-Clause "New" or "Revised" License
80 stars 5 forks source link

runEffIO :: Eff (IOEff) a -> IO a #2

Open kamoii opened 3 years ago

kamoii commented 3 years ago

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 like runEffIO :: Eff (IOEff) a -> IO a ? (or maybe I'm missing something and this is already possible?).

kamoii commented 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
kamoii commented 3 years ago

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.

kamoii commented 3 years ago

Tested with resolver lts-14.2, but same result.

kamoii commented 3 years ago

@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
xnning commented 3 years ago

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.

xnning commented 3 years ago

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.

xnning commented 3 years ago

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.

noughtmare commented 3 years ago

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.

noughtmare commented 3 years ago

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 }
xnning commented 3 years ago

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).

noughtmare commented 3 years ago

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.

noughtmare commented 3 years ago

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.

daanx commented 3 years ago

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)

noughtmare commented 3 years ago

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?