[Haskell-cafe] Execution Contexts

Benjamin Franksen benjamin.franksen at bessy.de
Fri Nov 26 19:59:32 EST 2004


I finally understood that George Russell's Library is not really about global 
variables. Rather it is about what I want to call 'execution contexts', which 
are -- as Marcin Kowalczyk observed -- a restricted form of dynamically 
scoped variables.

[NB: Another (maybe better) name would have been 'execution environment' but 
the name "environment" is too heavily associated with the related concept of 
process environment (the string to string map given to user processes as an 
implicit argument).]

An execution context is a mutable finite map from types to (monomorphic) 
values. Each IO action implicitly carries exactly one such map and by default 
passes it on to the actions that follow. A function is provided to 
(implicitly) create a new mapping and run a given IO action with the new 
mapping as its execution context, instead of the default one.

[NB: I also understand now why the library uses ThreadIds. This was obscure to 
me at first because in principle all this has nothing to do with concurrency 
(beside the requirement that accessing the context should be thread safe). 
ThreadIds are used simply because they are available as an index and nothing 
else is. Its just a hack.]

Seen this way, the whole thing smells very much of monads. Indeed, the monadic 
implementation is trivial. I attached a proof-of concept implementation, 
using George Russel's 'Dict' as an abstract data type in a separate module 
(copied verbatim from GlobalVariables.hs, see attached file Dict.hs). The 
idea: we define

type Context = MVar Dict

and introduce an eXtended version of the IO monad

type XIO a = StateT Context IO a

together with a small number of simple functions that implement the same 
interface as the original GlobalVariables.hs; no unsafe operations are used, 
everything is Haskell98 + Dynamics. Also ThreadIds do not appear and it is 
not necessary to change forkIO (apart from lifting it, of course). (code is 
in ExecutionContext.hs)

I modified George's test program so that it works with ExecutionContexts. The 
program is completely isomorphic to the original (and does the same, too ;). 
The only major difference is that all IO operations are lifted into the XIO 
monad. Again, almost everything is Haskell98, -fglasgow-exts is only needed 
to derive Typeable (which can also be done manually). (Code is in 
TestExecutionContext.hs)

The only task that remains to support this programming style so that it can be 
used practically, is to redefine IO as XIO in the kernel libraries. The 
annoying liftIOs everywhere (and the necessity to invent higher order lifts 
along the way) would be gone. I am almost sure that even the trick of 
indexing the dictionary via types (and thus the dependency on Data.Typeable 
and ghc extensions) can be avoided with a little more effort.

Ben
-------------- next part --------------
-- -----------------------------------------------------------------------
-- The Dict type
-- -----------------------------------------------------------------------
module Dict (
  Dict,
  emptyDict,
  lookupDict,
  addToDict,
  delFromDict
  ) where

import Data.Dynamic
import Data.Maybe

-- | Stores a set of elements with distinct types indexed by type
-- NB.  Needs to use a FiniteMap, when TypeRep's instance Ord.
newtype Dict = Dict [(TypeRep,Dynamic)]

-- | Dict with no elements.
emptyDict :: Dict
emptyDict = Dict []

-- | Retrieve an element from the dictionary, if one of that type exists.
lookupDict :: Typeable a => Dict -> Maybe a
lookupDict (Dict list) =
   let
      -- construct a dummy value of the required type so we can get at its
      -- TypeRep.
      Just dummy = (Just undefined) `asTypeOf` aOpt

      -- get at the required result type.
      dynOpt = lookup (typeOf dummy) list 

      aOpt = case dynOpt of
         Nothing -> Nothing
         Just dyn -> 
            Just (
               fromMaybe 
                  (error "Inconsistent type in Dict")
                  (fromDynamic dyn)
               )
   in
      aOpt

-- | Add an element to the dictionary if possible, or return Nothing if it
-- isn't because one of that type already exists.
addToDict :: Typeable a => Dict -> a -> Maybe Dict
addToDict (Dict list) val =
   let
      typeRep = typeOf val
   in
      case lookup typeRep list of
         Just _ -> Nothing
         Nothing -> Just (Dict ((typeRep,toDyn val) : list))
            
-- | Delete an element from the dictionary, if one is in it, or return Nothing
-- if it isn't.
delFromDict :: Typeable a 
   => Dict 
   -> a -- ^ this value is only interesting for its type, and isn't looked at.
   -> Maybe Dict
delFromDict (Dict list) val =
   let
      typeRep = typeOf val

      dList [] = Nothing
      dList ((hd@(typeRep2,_)):list2) = 
         if typeRep == typeRep2
            then
               Just list2
            else
               fmap (hd:) (dList list2)
   in
      fmap Dict (dList list)
-------------- next part --------------
module ExecutionContext where

import Control.Concurrent
import Control.Monad
import Control.Monad.State
import Data.Typeable

import Dict

type Context = MVar Dict

type XIO a = StateT Context IO a

-- evalStateT :: Monad m => StateT s m a -> s -> m a
runWithContext :: Context -> XIO a -> IO a
runWithContext ctx xio = evalStateT xio ctx

runWithEmptyContext :: XIO a -> IO a
runWithEmptyContext xio = do
  ctx <- newMVar emptyDict
  runWithContext ctx xio

lookupWithRegister :: Typeable a => IO a -> XIO a
lookupWithRegister xio = do
  ctx <- get
  -- modifyMVar :: MVar a -> (a -> IO (a, b)) -> IO b
  r <- liftIO $ modifyMVar ctx $ \dict -> do
    case lookupDict dict of
      Nothing -> do
        v <- xio
        let (Just dict') = addToDict dict v
        return (dict', v)
      Just v -> do
        return (dict, v)
  put ctx
  return r

withEmptyContext :: XIO a -> XIO a
withEmptyContext xio = liftIO $ runWithEmptyContext xio

liftIO2 :: (IO a -> IO b) -> XIO a -> XIO b
liftIO2 f xio = do
  ctx <- get
  liftIO $ f (runWithContext ctx xio)
-------------- next part --------------
module Main where

import Data.Typeable
import Data.IORef
import Control.Concurrent
import Control.Monad.Trans

import ExecutionContext

-- --------------------------------------------------------------------
-- Source of unique natural numbers
-- --------------------------------------------------------------------

data UniqueNaturalSource 
   = UniqueNaturalSource (IORef Integer) deriving (Typeable)

mkUniqueNaturalSource :: IO UniqueNaturalSource
mkUniqueNaturalSource =
   do
      ioRef <- newIORef 1
      return (UniqueNaturalSource ioRef)

getNextNatural :: XIO Integer
getNextNatural =
   do
      (UniqueNaturalSource ioRef) <- lookupWithRegister mkUniqueNaturalSource
      liftIO $ atomicModifyIORef ioRef (\ i -> (i+1,i))

-- --------------------------------------------------------------------
-- A little test program
-- --------------------------------------------------------------------

main :: IO ()
main =
   runWithEmptyContext $ do
      let
         p =
            do
               n <- getNextNatural
               liftIO $ putStrLn (show n)

      -- put in lots of forkIO's to make things interesting.
      let
         testNumbers i =
            do
               liftIO $ putStrLn ("Numbers starting at " ++ show i)
               wait <- liftIO $ newEmptyMVar

               liftIO2 forkIO (
                  do
                     p
                     p
                     p
                     liftIO2 forkIO (
                        do
                           p
                           liftIO2 forkIO (
                              do
                                 p
                                 liftIO $ putMVar wait () 
                              )
                           return ()
                        )
                     return ()
                  )
               liftIO $ takeMVar wait
      -- print 5 numbers beginning at 1.
      testNumbers 1
      -- print 5 numbers beginning at 1 again, with a new dictionary.
      withEmptyContext (testNumbers 1)

      -- print 5 numbers beginning at 6, still using the old dictionary
      testNumbers 6


More information about the Haskell-Cafe mailing list