[Haskell-cafe] Writing an IRC bot, problems with plugins

Bertram Felgenhauer bertram.felgenhauer at googlemail.com
Thu Nov 6 18:37:13 EST 2008


Alexander Foremny wrote:
> I am writing an single server, multi channel IRC bot with the support of
> plugins and limited plugin communication. With the plugin system I am facing
> problems I cannot really solve myself.

Here's an approach built completely around Data.Typeable. The
fundamental idea is that a Plugin encompasses a set of interfaces
of unknown types, which have Typeable instances.

All we need is an operation to extract such an interface from
a Plugin.

    data Plugin = Plugin {
        getInterface :: forall i. Typeable i => Maybe i
    }

Then we can define the interfaces we want to use, for example:

    data BaseInterface = BaseInterface {
        identifier :: String,
        rawMessage :: (MonadIO m) => Message -> PL m ()
    } deriving Typeable

Sending a message to a plugin can be implemented as

    sendMessage :: Plugin -> Message -> PL m ()
    sendMessage p msg = do
        let pI :: Maybe BaseInterface
            pI = getInterface p
        case pI of
            Nothing -> error "Plugin does not support BaseInterface"
            Just pI' -> rawMessage pI' msg

A more complete example follows below.

Does that help?

Bertram

------------------------------------------------------------------------

{-# LANGUAGE GADTs, Rank2Types, DeriveDataTypeable #-}

module PluginTest (main) where

import Data.Typeable
import Data.IORef
import Control.Monad.Trans
import Control.Monad.State
import qualified Data.Map as M

------------------------------------------------------------------------
-- Types

-- A Plugin is just a method that returns various interfaces.
data Plugin = Plugin {
    getInterface :: forall i. Typeable i => Maybe i
}

-- The basic interface.
--
-- It should be made a part of Plugin, but it's a queryable interface
-- in this example for demonstration purposes.
data BaseInterface = BaseInterface {
    identifier :: String,
    rawMessage :: (MonadIO m) => Message -> PL m ()
} deriving Typeable

type Message = String
type PL = StateT PluginConfig
type PluginConfig = M.Map String Plugin

------------------------------------------------------------------------
-- Main

-- look up a plugin by name
findPlugin :: Monad m => String -> PL m (Maybe Plugin)
findPlugin k = get >>= return . M.lookup k

-- register a plugin
registerPlugin :: MonadIO m => Plugin -> PL m ()
registerPlugin p = do
    -- note: 'getInterface' can return 'Nothing' - needs error checking
    let Just i = getInterface p
    modify (M.insert (identifier i) p)

-- unregister, etc.

main' :: MonadIO m => PL m ()
main' = do
    -- create two plugins (see below) and register them.
    a <- createAPlugin
    registerPlugin a
    b <- createBPlugin
    registerPlugin b
    -- extract base interfaces of a and b and send some messages
    -- (needs error checking)
    let aI, bI :: BaseInterface
        Just aI = getInterface a
        Just bI = getInterface b
    liftIO $ putStrLn "-> Sending message to A"
    rawMessage aI "dummy"
    liftIO $ putStrLn "-> Sending message to B"
    rawMessage bI "Hi, here's a message from B"
    liftIO $ putStrLn "-> Sending another message to A"
    rawMessage aI "dummy"

main :: IO ()
main = evalStateT main' M.empty

------------------------------------------------------------------------
-- Plugin A
--
-- This plugin provides an additional Interface that allows to
-- query and change a string value in its state.

data APlugin = APlugin (IORef String)
data AInterface = AInterface {
    aGet :: (MonadIO m) => PL m String,
    aPut :: (MonadIO m) => String -> PL m ()
} deriving Typeable

createAPlugin :: (MonadIO m) => PL m Plugin
createAPlugin = do
    r <- liftIO (newIORef "initial state")
    let a = APlugin r
    return $ Plugin {
            getInterface = cast (aBase a) `mplus` cast (aInterface a)
        }

aBase (APlugin r) = BaseInterface {
        identifier = "A",
        rawMessage = msg
    }
  where
    msg _ = liftIO $ do
       s <- readIORef r
       putStrLn ("A has state (" ++ s ++ ")!")


aInterface :: APlugin -> AInterface
aInterface (APlugin r) = AInterface {
        aGet = liftIO (readIORef r),
        aPut = \v -> liftIO (writeIORef r v)
    }

------------------------------------------------------------------------
-- Plugin B
--
-- Plugin B knows about Plugin A and uses its additional interface for
-- modifying its state

createBPlugin :: (MonadIO m) => PL m Plugin
createBPlugin = return $ Plugin {
        getInterface = cast bBase
    }

bBase = BaseInterface {
        identifier = "B",
        rawMessage = msg
    }
  where
    msg s = do
        -- find "A" plugin
        Just a <- findPlugin "A"
        -- and get its additional interface
        let aI :: AInterface
            Just aI = getInterface a
        aPut aI s


More information about the Haskell-Cafe mailing list