packages feed

immutaball-core-0.1.0.4.1: Immutaball/Share/ImmutaballIO.hs

{-# OPTIONS_GHC -fno-warn-tabs #-}  -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .
-- Enable warnings:
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}

-- ImmutaballIO.hs.

{-# LANGUAGE Haskell2010 #-}
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, InstanceSigs, ScopedTypeVariables, FlexibleContexts, UndecidableInstances, ExistentialQuantification #-}

-- TODO: also add monad and like instances for BasicIO and SDLIO with a Join
-- like we do here.
-- (Simple IOs like directoryIO probably don't need it.)

module Immutaball.Share.ImmutaballIO
	(
		-- * ImmutaballIO
		ImmutaballIO,
		ImmutaballIOF(..),
		runImmutaballIO,
		andImmutaballIO,
		thenImmutaballIO,
		(<>>),
		(<>>-),
		(<>-),
		joinImmutaballIOF,
		forkIBIOF,
		forkBoundIBIOF,
		forkBothBoundIBIOF,

		-- * mfix
		FixImmutaballIOException(..),
		fixImmutaballIOExceptionToException,
		fixImmutaballIOExceptionFromException,
		PrematureEvaluationFixImmutaballIOException(..),
		EmptyFixImmutaballIOException(..),
		fixImmutaballIOF,
		unsafeFixImmutaballIOFTo,

		-- * Runners
		runImmutaballIOIO,
		runBasicImmutaballIO,
		runDirectoryImmutaballIO,
		runSDLImmutaballIO,
		hArrayToBS,

		-- * ImmutaballIO aliases that apply the Fixed wrapper
		mkEmptyIBIO,
		mkPureIBIO,
		mkUnfixIBIO,
		mkJoinIBIO,
		mkAndIBIO,
		mkThenIBIO,
		mkBasicIBIO,
		mkWait,
		mkWithAsync,
		mkAtomically,
		mkThrowIO,
		mkArrayToBS,
		mkThawIO,
		mkFreezeIO,
		mkPoke,
		mkPeek,

		-- * Utils
		mkBIO
	) where

import Prelude ()
import Immutaball.Prelude

import GHC.Stack        (HasCallStack)      -- For ThrowIO.
--import Foreign.Ptr      (castPtr)           -- (For ArrayToBS.)
--import Foreign.Storable (sizeOf, Storable)  -- (For ArrayToBS.)

import Control.Concurrent.Async
import Control.Monad.Fix
import Control.Monad.STM
import Control.Parallel
import Data.Array.Base
import Data.Array.Storable
import qualified Data.ByteString as BS
import Foreign.Ptr
import Foreign.Storable

import Immutaball.Share.ImmutaballIO.BasicIO
import Immutaball.Share.ImmutaballIO.DirectoryIO
import Immutaball.Share.ImmutaballIO.SDLIO
import Immutaball.Share.Utils

-- (mfix imports.)
import Control.Concurrent.MVar
import Control.Exception
import Data.Typeable
import GHC.IO.Unsafe (unsafeDupableInterleaveIO)
import System.IO.Unsafe (unsafePerformIO)

-- * ImmutaballIO

type ImmutaballIO = Fixed ImmutaballIOF
data ImmutaballIOF me =
	  EmptyIBIOF
	| PureIBIOF me
	| UnfixIBIOF (ImmutaballIOF me)
	| JoinIBIOF (ImmutaballIOF (ImmutaballIOF me))
	| AndIBIOF me me
	| ThenIBIOF me me
	| BasicIBIOF (BasicIOF me)

	| forall hiddenTypeField. Wait (Async hiddenTypeField) (hiddenTypeField -> me)
	| WithAsync me (Async () -> me)
	| forall hiddenTypeField. Atomically (STM hiddenTypeField) (hiddenTypeField -> me)

	| forall e. (HasCallStack, Exception e) => ThrowIO e

	| forall i e. (Integral i, Ix i, Storable e) => ArrayToBS (StorableArray i e) (BS.ByteString -> me)

	| forall a b i e. (Ix i, IArray a e,    MArray b e IO) => ThawIO   (a i e) (b i e -> me)
	| forall a b i e. (Ix i, MArray a e IO, IArray b e)    => FreezeIO (a i e) (b i e -> me)

	| forall a. (Storable a) => Poke (Ptr a) a me
	| forall a. (Storable a) => Peek (Ptr a) (a -> me)

runImmutaballIO :: ImmutaballIO -> IO ()
runImmutaballIO bio = cata runImmutaballIOIO bio

andImmutaballIO :: ImmutaballIO -> ImmutaballIO -> ImmutaballIO 
andImmutaballIO x y = Fixed $ AndIBIOF x y

thenImmutaballIO :: ImmutaballIO -> ImmutaballIO -> ImmutaballIO
thenImmutaballIO x y = Fixed $ ThenIBIOF x y

instance Semigroup (ImmutaballIOF ImmutaballIO) where
	(<>) :: ImmutaballIOF ImmutaballIO -> ImmutaballIOF ImmutaballIO -> ImmutaballIOF ImmutaballIO
	a <> b = AndIBIOF (Fixed a) (Fixed b)
instance (Semigroup (ImmutaballIOF me)) => Monoid (ImmutaballIOF me) where
	mempty = EmptyIBIOF

instance Semigroup ImmutaballIO where
	(Fixed a) <> (Fixed b) = Fixed (a <> b)
instance Monoid ImmutaballIO where
	mempty = Fixed EmptyIBIOF

instance Functor ImmutaballIOF where
	fmap :: (a -> b) -> (ImmutaballIOF a -> ImmutaballIOF b)
	fmap _f (EmptyIBIOF)      = EmptyIBIOF
	fmap  f (PureIBIOF a)     = PureIBIOF (f a)
	fmap  f (UnfixIBIOF ibio) = UnfixIBIOF (f <$> ibio)
	fmap  f (JoinIBIOF ibio)  = JoinIBIOF (fmap f <$> ibio)
	fmap  f (AndIBIOF a b)    = AndIBIOF (f a) (f b)
	fmap  f (ThenIBIOF a b)   = ThenIBIOF (f a) (f b)
	fmap  f (BasicIBIOF bio)  = BasicIBIOF $ f <$> bio

	fmap  f (Wait async_ withAsync_)    = Wait async_ (f . withAsync_)
	fmap  f (WithAsync ibio withAsync_) = WithAsync (f ibio) (f . withAsync_)
	fmap  f (Atomically stm withStm)    = Atomically stm (f . withStm)

	fmap _f (ThrowIO e)       = ThrowIO e

	fmap  f (ArrayToBS array_ withBS) = ArrayToBS array_ (f . withBS)

	fmap  f (ThawIO   iarray withMArray) = ThawIO   iarray (f . withMArray)
	fmap  f (FreezeIO marray withIArray) = FreezeIO marray (f . withIArray)

	fmap  f (Poke ptr val withUnit) = Poke ptr val (f withUnit)
	fmap  f (Peek ptr     withVal)  = Peek ptr     (f . withVal)

joinImmutaballIOF :: ImmutaballIOF (ImmutaballIOF a) -> ImmutaballIOF a
joinImmutaballIOF = JoinIBIOF

-- | Fork the first IBIOF, with the result associated with the second IBIOF.
--
-- This is represented as a flipped And, since And is normally understood to
-- have its first _ordinary_ ‘me’ (e.g. not a bound thread like ForkOS) to be
-- the result.  This mostly pertains to mfix: e.g. fix with And takes the first
-- argument as the result, but still expands the tree to traverse through the
-- second too, preserving ‘let ma = ma >>= f’ in a computable manner.  mfix
-- with forkOS takes the second argument as result first, but still traverses
-- the tree all the same.
--
-- Note: due to the underlying 'concurrently_' runner, both branches are run
-- under an unbound thread.
--
-- Use 'forkBoundIBIOF' or 'ForkIO' if instead you wish to keep the primary
-- thread in the second argument bound, but fork the first unbound.  Use
-- 'forkBothboundIBIOF' or 'ForkOS' if you wish to keep both bound.  Ensuring
-- some commands are run in a same bound thread may be necessary e.g. for
-- OpenGL.
forkIBIOF :: ImmutaballIOF me -> ImmutaballIOF me -> ImmutaballIOF me
forkIBIOF fork withUnit = JoinIBIOF $ flip AndIBIOF fork withUnit

-- | Keep the second, main thread in a bound thread if it is running in a bound thread.
--
-- Note the underlying runner does not use 'concurrently_' to manage automatic
-- thread clean-up.  Thus normally 'forkIBIOF' is preferable unless you want to
-- keep the second argument running in a bound thread, e.g. for OpenGL rendering.
--
-- (See 'Control.Concurrent' documentation for what a bound versus unbound
-- thread means.)
forkBoundIBIOF :: ImmutaballIOF me -> ImmutaballIOF me -> ImmutaballIOF me
forkBoundIBIOF fork withUnit = JoinIBIOF . BasicIBIOF $ ForkIO fork withUnit

-- | Fork into a bound thread with 'forkOS'.
forkBothBoundIBIOF :: ImmutaballIOF me -> ImmutaballIOF me -> ImmutaballIOF me
forkBothBoundIBIOF fork withUnit = JoinIBIOF . BasicIBIOF $ ForkOS fork withUnit

-- * mfix

data FixImmutaballIOException = forall e. Exception e => FixImmutaballIOException e
instance Show FixImmutaballIOException where
	show (FixImmutaballIOException e) = show e
instance Exception FixImmutaballIOException
fixImmutaballIOExceptionToException :: Exception e => e -> SomeException
fixImmutaballIOExceptionToException = toException . FixImmutaballIOException
fixImmutaballIOExceptionFromException :: Exception e => SomeException -> Maybe e
fixImmutaballIOExceptionFromException x = do
	FixImmutaballIOException a <- fromException x
	cast a

-- | Premature evaluation or attempt to access an empty value (e.g. mfix with EmptyIBIOF).
data PrematureEvaluationFixImmutaballIOException = PrematureEvaluationFixImmutaballIOException
	deriving (Show)
instance Exception PrematureEvaluationFixImmutaballIOException where
	toException = fixImmutaballIOExceptionToException
	fromException = fixImmutaballIOExceptionFromException

data EmptyFixImmutaballIOException = EmptyFixImmutaballIOException
	deriving (Show)
instance Exception EmptyFixImmutaballIOException where
	toException = fixImmutaballIOExceptionToException
	fromException = fixImmutaballIOExceptionFromException

--    mfix f = mfix f >>= f
-- => mfix f = join $ f <$> mfix f
-- Incorrect: runs f twice.
	--x -> f undefined >>= mfix f
{-
fixImmutaballIOF :: (me -> ImmutaballIOF me) -> ImmutaballIOF me
fixImmutaballIOF f = case f (error "Error: fixImmutaballIOF: premature evaluation of result before we could start it!") of
	x -> joinImmutaballIOF $ f <$> x
-}
-- Do it like fixIO and fixST (see also their notes; it's a little tricky).
-- Use a lazily read MVar.
fixImmutaballIOF :: (me -> ImmutaballIOF me) -> ImmutaballIOF me
fixImmutaballIOF f = unsafePerformIO $ do
	mme <- newEmptyMVar
	return $ unsafeFixImmutaballIOFTo mme f

-- | Helper for fixImmutaballIOF.
unsafeFixImmutaballIOFTo :: MVar me -> (me -> ImmutaballIOF me) -> ImmutaballIOF me
unsafeFixImmutaballIOFTo mme f = unsafePerformIO $ do
	me_ <- unsafeDupableInterleaveIO (readMVar mme `catch` \BlockedIndefinitelyOnMVar -> throwIO PrematureEvaluationFixImmutaballIOException)
	case f me_ of
		--_y@(EmptyIBIOF)        -> throwIO EmptyFixImmutaballIOException
		_y@(EmptyIBIOF)        -> return $ EmptyIBIOF
		y@(PureIBIOF a)        -> putMVar mme a >> return y
		_y@(UnfixIBIOF ibio)   -> return . UnfixIBIOF . unsafeFixImmutaballIOFTo mme $ const ibio
		-- Join: Cover all multi-branching (or else we could hang on multiple putMVars), then just fmap for all other cases.
		_y@(JoinIBIOF (AndIBIOF a b)) -> return (JoinIBIOF (AndIBIOF (unsafeFixImmutaballIOFTo mme (const a)) (JoinIBIOF $ f <$> b)))
		_y@(JoinIBIOF (ThenIBIOF a b)) -> return (JoinIBIOF (ThenIBIOF (unsafeFixImmutaballIOFTo mme (const a)) (JoinIBIOF $ f <$> b)))
		_y@(JoinIBIOF ibio)    -> return $ JoinIBIOF (unsafeFixImmutaballIOFTo mme . const <$> ibio)
		_y@(AndIBIOF  a b)     -> putMVar mme a >> return (JoinIBIOF $ AndIBIOF  (PureIBIOF a) (f b))
		_y@(ThenIBIOF a b)     -> putMVar mme a >> return (JoinIBIOF $ ThenIBIOF (PureIBIOF a) (f b))
		_y@(BasicIBIOF bio)    -> return . BasicIBIOF . unsafeFixBasicIOFTo mme $ const bio

		_y@(Wait async_ withAsync_)    -> return $ Wait       async_ ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withAsync_)
		_y@(WithAsync ibio withAsync_) -> return $ WithAsync  ibio   ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withAsync_)
		_y@(Atomically stm withStm)    -> return $ Atomically stm    ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withStm)

		--y@(ThrowIO _e me) -> putMVar mme me >> return y
		_y@(ThrowIO e) -> return $ ThrowIO e

		_y@(ArrayToBS array_ withBS) -> return $ ArrayToBS array_ ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withBS)

		_y@(ThawIO   iarray withMArray) -> return $ ThawIO   iarray ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withMArray)
		_y@(FreezeIO marray withIArray) -> return $ FreezeIO marray ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withIArray)

		y@( Poke _ptr _val me)      -> putMVar mme me >> return y
		_y@(Peek ptr       withVal) -> return $ Peek ptr ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withVal)

instance Applicative ImmutaballIOF where
	pure = PureIBIOF
	mf <*> ma = joinImmutaballIOF . flip fmap mf $ \f -> joinImmutaballIOF .  flip fmap ma $ \a -> pure (f a)
instance Monad ImmutaballIOF where
	return = pure
	m >>= f = joinImmutaballIOF $ f <$> m
instance MonadFix ImmutaballIOF where
	mfix :: (a -> ImmutaballIOF a) -> ImmutaballIOF a
	mfix = fixImmutaballIOF

{-
instance Foldable ImmutaballIOF where
	foldr :: (a -> b -> b) -> b -> ImmutaballIOF a -> b
	foldr _reduce reduction0 (EmptyIBIOF)  = reduction0
	foldr  reduce reduction0 (PureIBIOF a) = reduce a reduction0
	foldr  reduce reduction0 (JoinIBIOF ibio) = ??? $ foldr reduce reduction0 <$> ibio
-}

-- | Add an ordering constraint.
infixr 6 <>>
(<>>) :: ImmutaballIO -> ImmutaballIO -> ImmutaballIO
(<>>) = thenImmutaballIO

-- | A more general version.
infixr 6 <>>-
(<>>-) :: ImmutaballIOF me -> ImmutaballIOF me -> ImmutaballIOF me
a <>>- b = JoinIBIOF $ a `ThenIBIOF` b

-- | Without an ordering constraint.
infixr 6 <>-
(<>-) :: ImmutaballIOF me -> ImmutaballIOF me -> ImmutaballIOF me
a <>- b = JoinIBIOF $ a `AndIBIOF` b

-- * Runners

runImmutaballIOIO :: ImmutaballIOF (IO ()) -> IO ()

runImmutaballIOIO (EmptyIBIOF)      = return ()
runImmutaballIOIO (PureIBIOF a)     = a
runImmutaballIOIO (UnfixIBIOF ibio) = runImmutaballIOIO ibio
runImmutaballIOIO (JoinIBIOF ibio)  = runImmutaballIOIO $ runImmutaballIOIO <$> ibio
runImmutaballIOIO (AndIBIOF a b)    = a `par` b `par` concurrently_ a b
runImmutaballIOIO (ThenIBIOF a b)   = a >> b
runImmutaballIOIO (BasicIBIOF bio)  = runBasicIOIO bio

runImmutaballIOIO (Wait async_ withAsync_)    = wait async_ >>= withAsync_
runImmutaballIOIO (WithAsync ibio withAsync_) = withAsync ibio withAsync_
runImmutaballIOIO (Atomically stm withStm)    = atomically stm >>= withStm

runImmutaballIOIO (ThrowIO e) = throwIO e

runImmutaballIOIO (ArrayToBS array_ withBS) = hArrayToBS array_ >>= withBS

runImmutaballIOIO (ThawIO   iarray withMArray) = thaw   iarray >>= withMArray
runImmutaballIOIO (FreezeIO marray withIArray) = freeze marray >>= withIArray

runImmutaballIOIO (Poke ptr val ibio)    = poke ptr val >> ibio
runImmutaballIOIO (Peek ptr     withVal) = peek ptr     >>= withVal

runBasicImmutaballIO :: BasicIO -> ImmutaballIO
runBasicImmutaballIO bio = Fixed $ BasicIBIOF (runBasicImmutaballIO <$> getFixed bio)

runDirectoryImmutaballIO :: DirectoryIO -> ImmutaballIO
runDirectoryImmutaballIO dio = runBasicImmutaballIO . runDirectoryBasicIO $ dio

runSDLImmutaballIO :: SDLIO -> ImmutaballIO
runSDLImmutaballIO sdlio = runBasicImmutaballIO . runSDLBasicIO $ sdlio

-- | Copy an array to an immutable strict bytestring.
hArrayToBS :: (Integral i, Ix i, Storable e) => StorableArray i e -> IO BS.ByteString
hArrayToBS array_ = do
	(len :: Integer) <- fromIntegral <$> getNumElements array_
	size <- do
		if len <= 0
			then return 0
			else do
				arrayHead <- readArray array_ (fromIntegral (0 :: Integer))
				let elemSize = fromIntegral $ sizeOf arrayHead
				let size_ = len * elemSize
				return size_
	bs <- withStorableArray array_ $ \ptr -> BS.packCStringLen ((castPtr ptr), fromIntegral $ size)
	return bs

-- * ImutaballIO aliases that apply the Fixed wrapper

mkEmptyIBIO :: ImmutaballIO
mkEmptyIBIO = Fixed $ EmptyIBIOF

mkPureIBIO :: ImmutaballIO -> ImmutaballIO
mkPureIBIO ibio = Fixed $ PureIBIOF ibio

mkUnfixIBIO :: ImmutaballIO -> ImmutaballIO
mkUnfixIBIO ibio = Fixed $ UnfixIBIOF (getFixed ibio)

mkJoinIBIO :: ImmutaballIO -> ImmutaballIO
mkJoinIBIO ibio = Fixed $ JoinIBIOF (getFixed <$> getFixed ibio)

mkAndIBIO :: ImmutaballIO -> ImmutaballIO -> ImmutaballIO
mkAndIBIO a b = Fixed $ AndIBIOF a b

mkThenIBIO :: ImmutaballIO -> ImmutaballIO -> ImmutaballIO
mkThenIBIO a b = Fixed $ ThenIBIOF a b

mkBasicIBIO :: BasicIOF ImmutaballIO -> ImmutaballIO
mkBasicIBIO bio = Fixed $ BasicIBIOF bio

mkWait :: Async a -> (a -> ImmutaballIO) -> ImmutaballIO
mkWait async_ withAsync_ = Fixed $ Wait async_ withAsync_

mkWithAsync :: ImmutaballIO -> (Async () -> ImmutaballIO) -> ImmutaballIO
mkWithAsync ibio withAsync_ = Fixed $ WithAsync ibio withAsync_

mkAtomically :: STM a -> (a -> ImmutaballIO) -> ImmutaballIO
mkAtomically stm withStm = Fixed $ Atomically stm withStm

mkThrowIO :: (HasCallStack, Exception e) => e -> ImmutaballIO
mkThrowIO e = Fixed $ ThrowIO e

mkArrayToBS :: (Integral i, Ix i, Storable e) => StorableArray i e -> (BS.ByteString -> ImmutaballIO) -> ImmutaballIO
mkArrayToBS array_ withBS = Fixed $ ArrayToBS array_ withBS

mkThawIO :: (Ix i, IArray a e, MArray b e IO) => a i e -> (b i e -> ImmutaballIO) -> ImmutaballIO
mkThawIO iarray withMArray = Fixed $ ThawIO iarray withMArray

mkFreezeIO :: (Ix i, MArray a e IO, IArray b e) => a i e -> (b i e -> ImmutaballIO) -> ImmutaballIO
mkFreezeIO marray withIArray = Fixed $ FreezeIO marray withIArray

mkPoke :: (Storable a) => Ptr a -> a -> ImmutaballIO -> ImmutaballIO
mkPoke ptr val ibio = Fixed $ Poke ptr val ibio

mkPeek :: (Storable a) => Ptr a -> (a -> ImmutaballIO) -> ImmutaballIO
mkPeek ptr withVal = Fixed $ Peek ptr withVal

-- * Utils

-- | Short alias for 'mkBasicImmutaballIO'.
mkBIO :: BasicIOF ImmutaballIO -> ImmutaballIO
mkBIO = mkBasicIBIO