[Haskell-cafe] Safely polymorphic unsafePerformIO

Joel Reymont joelr1 at gmail.com
Mon Dec 12 11:46:31 EST 2005

On Dec 12, 2005, at 11:31 AM, Bulat Ziganshin wrote:
> Hello Joel,
> this code really looks strange: you asks to create global veriable,
> but don't say its type :)  polymorhism is for functions definitions,
> any concrete data in Haskell have concrete type

It's a long story but I'll try to explain. I would also emphasize  
that the code works and the type checker complains if I try to use,  
say, Event String once Event Int has been used.

So this is how I do it...

This chunk lives in my internal libraries that I deliver to the  
client. I have some pre-defined events and let the user of the  
library come up with custom ones.

data Event a
     = Go
     | Quit
     | ForcedQuit
     | NetworkError Exception
     | Timeout String
     | Cmd Command
     | Custom a
     deriving Show

Yes, this looks bad but keep reading! The 'a' is the same throughout.

type Child a = (MVar (), TMVar (ClockTime, (Event a)), MVar ThreadId)

{-# NOINLINE children #-}
children :: MVar [Child a]
children = unsafePerformIO $ newMVar []

forkChild :: Show a => (TMVar (ClockTime, (Event a)) -> IO ()) -> IO  
forkChild io =
     do mvar <- newEmptyMVar
        mbx <- atomically $ newEmptyTMVar
        childs <- takeMVar children
        thread <- newEmptyMVar
        putMVar children ((mvar, mbx, thread):childs)
        tid <- forkIO (io mbx `finally` putMVar mvar ())
        putMVar thread tid
        return tid

This is the poker bot state. I use the 'b' for the user data type.

data World a b = World
      dispatchers :: ![(String, Dispatcher a b)],
      trace_filter:: Event a -> Bool,
      user_data :: !(Maybe b)

The monad...

type ScriptState a b = ErrorT String (StateT (World a b) IO)
type ScriptResult a b = IO (Either String (), World a b)

This is the type signature for the bot fun...

type Dispatcher a b = Event a -> ((ScriptState a b) (Status a))

data Status a
     = Start
     | Eat (Maybe (Event a))
     | Skip
     deriving Show

What each bot should return. Eat means do not process any further  
dispatchers in the list of dispatchers kept in the bot state (World  
above). Skip will continue processing by calling dispatchers upstream  
with the same event.

Dispatchers can fail thus they are in the ScriptState monad.

This bit is actually exported

getdata :: Show b => (ScriptState a b) b
getdata =
     do w <- get
        return $ fromJust $ user_data w

setdata :: b -> (ScriptState a b) ()
setdata b =
     do w <- get
        put_ $ w { user_data = Just $ b }

launch :: (Show a, Show b) =>
           HostInfo -> Dispatcher a b -> IO ()
launch hi script =
     do forkChild $ run hi script
        liftIO $ sleep_ 10
        return ()

This is what a user "script" looks like. We are almost there, I promise!

data CustomEvent
     = Tables [TableInfo]
     | LoggedIn
     | JoinedTable Word32
     | SeatTaken Word8
     | SeatNotTaken Word8
     | DealerChip
     | Quorum
     deriving Show

main =
     do initSnippets
        launch host script
        sleep_ 2000 -- 2 seconds

Notice the call to setdata (). The type of 'b' will not be known  
without it and ghc will not compile the program.

script Go =
     do startScript
        setdata ()
        trace 10 "Kicking off"
        push "goToLobby" $ goToLobby [28]
        return $ Eat $ Just Go

script (Custom (JoinedTable 0)) =
     do trace 10 "We are in the lobby"
        return $ Eat $ Just Quit

script (Timeout _) =
     fail $ "Script: Timeout received"

script event =
     do fail $ "script: event: " ++ show event
        return Skip

Now, as soom as I use my custom event in the script, 'a' in the  
dispatcher signature and everywhere else will be "bound" to  
CustomEvent and thus the chunk of code below will be resolved.

{-# NOINLINE children #-}
children :: MVar [Child a]
children = unsafePerformIO $ newMVar []

As soon as you try to post a different event somewhere in the script  
ghc will complain of a type mismatch and suggest that you use  
CustomEvent instead. Problem solved, everything works.

Did I explain this to everyone's satisfaction? Have I supplied enough  
context?  Is my code beautiful and efficient?

Finally, does anyone have _constructive_ criticism to offer? :D

	Thanks, Joel


More information about the Haskell-Cafe mailing list