Global Variables and IO initializers
Thanks to the encouraging post http://www.haskell.org//pipermail/haskell/2004-November/014748.html from Benjamin Franksen, I have implemented my proposal which allows the user to define new global variables without unsafePerformIO, NOINLINE and other such horrors. http://www.haskell.org//pipermail/haskell/2004-November/014748.html The module itself is in GlobalVariables.hs; a short example of its use and a test case (generating unique natural numbers) is in TestGlobalVariables.hs. Both files are attached to this message. Here are some things I think people should like about this method. (1) It's typesafe. (2) The order in which things happen is well-defined, provided that the order in which the external functions are called is. (3) It needs no extensions to the Haskell language, and only fairly standard hierarchical libraries like Data.IORef. (4) With withEmptyDict, you can run actions within your program that start with a completely clean slate and don't clobber your existing global variables. (This could be used if you are GHCi and want to run code inside a sandbox, or if you have separate processors and want each to have its own global variables.) (5) We avoid any concurrency primitives which might block (no MVars!). Some things people might not like so much. (1) You are not allowed to have two global variables with the same type (since they are indexed by type. (2) Global variables can only be accessed by an IO action. (You could not implement stdout :: Handle, you'd have to implement getstdout :: IO Handle.) (3) To implement withEmptyDict, it is necessary to (a) use ThreadId's, which means the code won't work on Hugs; (b) use a special version of forkIO (whose implementation is included) which makes sure new dictionaries are inherited. (4) Dictionaries are unnecessarily slow (linear access time in numbers of entries). Neither (1) or (2) bother me much, because I virtually always use global variables like that anyway. I hope (3)(a) will be addressed someday by Hugs implementing ThreadId's, and for (3)(b) I would suggest either making this the default forkIO action, or providing a "Who's my parent" primitive. (4) would be easy to resolve if Data.Dynamic.TypeRep were made to instance Ord, which I think should be trivial (for GHC at least). Anyway, it's not perfect, but I think it's the best solution, and I condemn it to haskell.org ... {- Program to test global variables by implementing a source of unique natural numbers. -} module Main where import Data.Dynamic import Data.IORef import Data.GlobalVariables import Control.Concurrent.MVar -- -------------------------------------------------------------------- -- Source of unique natural numbers -- -------------------------------------------------------------------- data UniqueNaturalSource = UniqueNaturalSource (IORef Integer) deriving (Typeable) mkUniqueNaturalSource :: IO UniqueNaturalSource mkUniqueNaturalSource = do ioRef <- newIORef 1 return (UniqueNaturalSource ioRef) getNextNatural :: IO Integer getNextNatural = do (UniqueNaturalSource ioRef) <- lookupWithRegister mkUniqueNaturalSource atomicModifyIORef ioRef (\ i -> (i+1,i)) -- -------------------------------------------------------------------- -- A little test program -- -------------------------------------------------------------------- main :: IO () main = do let p = do n <- getNextNatural putStrLn (show n) -- put in lots of forkIO's to make things interesting. let testNumbers i = do putStrLn ("Numbers starting at " ++ show i) wait <- newEmptyMVar forkIO ( do p p p forkIO ( do p forkIO ( do p putMVar wait () ) return () ) return () ) takeMVar wait -- print 5 numbers beginning at 1. testNumbers 1 -- print 5 numbers beginning at 1 again, with a new dictionary. withEmptyDict (testNumbers 1) -- print 5 numbers beginning at 6, still using the old dictionary testNumbers 6 -- | -- Description: Tool for initialising global variables -- -- At the moment this is inefficient, mainly because of the pitiful -- support by the standard libraries for indexing on TypeRep's and ThreadId's. -- Someone please make TypeRep instance Ord and provide a hash function for -- ThreadId!! module Data.GlobalVariables( lookupWithRegister, withEmptyDict, withFreshDict, forkIO, ) where import Maybe import List import Data.IORef import Data.Dynamic import Data.FiniteMap import System.IO.Unsafe import Control.Exception import Control.Concurrent hiding (forkIO) import qualified Control.Concurrent -- ----------------------------------------------------------------------- -- The user interface -- ----------------------------------------------------------------------- -- | Look up some global variable. The initial action constructs it if -- necessary. lookupWithRegister :: Typeable a => IO a -> IO a lookupWithRegister initialisationAction = do let updateFn dict = case lookupDict dict of Just a -> (dict,a) Nothing -> let a = unsafePerformIO initialisationAction Just dict2 = addToDict dict a in (dict2,a) a <- updateDictState updateFn seq a (return a) -- | Perform some action and the child threads it splits off with -- an alternative dictionary which is initially empty. withEmptyDict :: IO a -> IO a withEmptyDict = withOtherDict (\ _ -> emptyDict) -- | Perform some action and the child threads it splits off with -- a fresh dictionary split off from the current one. withFreshDict :: IO a -> IO a withFreshDict = withOtherDict id -- | Perform some action and the child threads it splits off with -- a dictionary computed from the current one. withOtherDict :: (Dict -> Dict) -> IO a -> IO a withOtherDict newDictFn action = do oldDictRefOpt <- getDictRefOpt oldDict <- readIORef (case oldDictRefOpt of Nothing -> defaultDict theDictState Just oldDictRef -> oldDictRef ) let newDict = newDictFn oldDict newDictRef <- newIORef newDict setDictRef newDictRef finally action (case oldDictRefOpt of Nothing -> forgetDict Just oldDictRef -> setDictRef oldDictRef ) -- ----------------------------------------------------------------------- -- The Dict type -- ----------------------------------------------------------------------- -- | 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) -- ------------------------------------------------------------------------- -- The DictState type. This should contain all your program's global state. -- ------------------------------------------------------------------------- data DictState = DictState { stateRef :: IORef (FiniteMap ThreadId (IORef Dict)), defaultDict :: IORef Dict -- this corresponds to the main program and is used when we can't -- determine a dictionary for a thread. } theDictState :: DictState theDictState = unsafePerformIO ( do stateRef <- newIORef emptyFM defaultDict <- newIORef emptyDict return (DictState {stateRef = stateRef,defaultDict = defaultDict}) ) {-# NOINLINE theDictState #-} -- | Get the current DictRef for a thread getDictRef :: IO (IORef Dict) getDictRef = do dictRefOpt <- getDictRefOpt return (fromMaybe (defaultDict theDictState) dictRefOpt) -- | Update the dictionary for this thread with the given update function -- and returning the result. updateDictState :: (Dict -> (Dict,result)) -> IO result updateDictState modifyFn = do dictRef <- getDictRef atomicModifyIORef dictRef modifyFn -- | Get the dictionary for this thread, if any. getDictRefOpt :: IO (Maybe (IORef Dict)) getDictRefOpt = do threadId <- myThreadId fm0 <- readIORef (stateRef theDictState) return (lookupFM fm0 threadId) -- | Set the dictionary ref for this thread. setDictRef :: IORef Dict -> IO () setDictRef dictRef = do threadId <- myThreadId atomicModifyIORef (stateRef theDictState) (\ fm0 -> (addToFM fm0 threadId dictRef,())) -- | Forget this thread's dictionary. (This is necessary for garbage -- collection.) forgetDict :: IO () forgetDict = do threadId <- myThreadId atomicModifyIORef (stateRef theDictState) (\ fm0 -> (delFromFM fm0 threadId,())) -- Use as a substitute for normal forkIO, so that dictionaries get -- inherited. forkIO :: IO () -> IO ThreadId forkIO actToFork = do dictRefOpt <- getDictRefOpt case dictRefOpt of Nothing -> Control.Concurrent.forkIO actToFork Just dictRef -> let actToFork2 = finally ( do setDictRef dictRef actToFork ) forgetDict in Control.Concurrent.forkIO actToFork2
Lennart Augustsson wrote:
George Russell wrote:
(3) It needs no extensions to the Haskell language, and only fairly standard hierarchical libraries like Data.IORef.
It uses unsafePerformIO which is very much an extension to Haskell. :)
I think by Haskell he means the common language currently implemented by GHC and Hugs. -- Ben
[encouraging everybody to reply on haskell-cafe] On Tuesday 23 November 2004 12:02, you wrote:
Thanks to the encouraging post
http://www.haskell.org//pipermail/haskell/2004-November/014748.html
from Benjamin Franksen, I have implemented my proposal which allows the user to define new global variables without unsafePerformIO, NOINLINE and other such horrors.
This is funny. When I got no immediate reaction from you, I started implementing it myself. I ended up with something similar. It has less features but is also a lot simpler. This is the interface: initGlobal :: Typeable a => a -> IO () getGlobal :: Typeable a => IO a Some remarks: o The separation into two modules is only historical. o I use an MVar internally, not an IORef; since it is not exposed, no indefinite blocking can occur. It's just a mutex around the dictionary. o Storing (TypeRep,Dynamic) pairs is redundant, since Dynamics already contain their own TypeRep (that is how they are made to work). o Both our implementations use unsafePerformIO in an unsafe manner, which is why the NOINLINE flag is used. o I also use a list for the dictionary; and I share your view about TypeRep badly needing an Ord instance (probably trivial to provide but I could be wrong). *************** On a related note, there was some discussion recently about which IO actions should be considered as 'harmless' so that they are allowed for the proposed top-level '<-' bindings, and how to characterize them in an elegant way. Here is yet another solution: The only things allowed at top-level (other than pure values) will be unique labels (such as provided by Data.Unique). Then we take all the newXXX actions out of whatever monad they live in and provide them as pure functions that take a Unique as additional argument: newXXX :: Unique -> a -> XXX a This might be a bit tricky to do efficiently. Anyway, a Haskell program could then create top-level Unique labels instead of top-level XXX vars: myGlobalVarLabel <- unique myGlobalMVar = newMVar myGlobalVarLabel "initial content" The concrete syntax could be made even simpler (and clearer), i.e. without the '<-' operator: unique myGlobalVarLabel Advantages: o No question of what is in SafeIO and what is not. o No question of when top-level IO actions are performed. *************** Now what about combining the two aproaches? The point here is that in Haskell we can easily create new unique labels at the top-level without resorting to any kind of unsafe operations: data Uniq1 = Uniq1 data Uniq2 = Uniq2 ... Only these have not the same but different types. So we need a way to map them to a single type in such a way that their uniqueness is preserved. We already have such a tool and it is called "deriving Typeable": unique = typeOf type Unique = TypeRep data Uniq1 = Uniq1 deriving Typeable data Uniq2 = Uniq2 deriving Typeable Our unique labels can now simply be defined as label1 = unique Uniq1 label2 = unique Uniq2 and our global variables as global1 = functionalNewMVar label1 True global2 = functionalNewMVar label1 (117::Int) I think this is most elegant, although there remains the questions of an efficient implementation of functionalNewXXX. Ben
participants (4)
-
Ben Rudiak-Gould -
Benjamin Franksen -
George Russell -
Lennart Augustsson