Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
kamoii opened this issue Mar 11, 2021 · 14 comments · May be fixed by #4
Open

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

kamoii opened this issue Mar 11, 2021 · 14 comments · May be fixed by #4

Comments

@kamoii
Copy link

kamoii commented Mar 11, 2021

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
Copy link
Author

kamoii commented Mar 11, 2021

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
Copy link
Author

kamoii commented Mar 11, 2021

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
Copy link
Author

kamoii commented Mar 11, 2021

Tested with resolver lts-14.2, but same result.

@xnning xnning mentioned this issue Mar 12, 2021
@kamoii
Copy link
Author

kamoii commented Mar 13, 2021

@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
Copy link
Owner

xnning commented Mar 20, 2021

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
Copy link
Owner

xnning commented Mar 20, 2021

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
Copy link
Owner

xnning commented Mar 20, 2021

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
Copy link

noughtmare commented Mar 20, 2021

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
Copy link

noughtmare commented Mar 20, 2021

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
Copy link
Owner

xnning commented Mar 20, 2021

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 noughtmare linked a pull request Mar 21, 2021 that will close this issue
@noughtmare
Copy link

noughtmare commented Mar 28, 2021

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
Copy link

noughtmare commented May 1, 2021

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
Copy link
Collaborator

daanx commented May 1, 2021

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
Copy link

noughtmare commented May 2, 2021

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants