Hello, Community.
I already bothered #haskell a few times with
the very same problem and always got interesting responses. But I seem
to have always simplified my problem too much ending up with an helpful
answer to my (simplified) problem, but not with a real solution. Thus
I'm giving the mailing list a try where I don't feel like having to
wrap up my concerns in as few lines as possible.
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.
My
general idea is to have the main application listening to the network
socket and then calling all the plugins on each incoming message.
Therefore I maintain a list of plugin states in the main application's
state and on each incoming message I call a function which modifies the
plugin's state.
There's a PluginClass class which contains definitions of functions
for each plugin which they all share. Simplyfied it's like this:
type PL = StateT PluginConfig
class PluginClass a where
identifier :: a -> String
rawMessage :: (MonadIO m) => a -> Message -> PL m ()
So plugins can be identified uniquely using the identifier function
and they can respond to messages using the rawMessage function. This
function is executed in the PL monad, which is essentially a StateT
monad for updating the plugin's state trough put and maybe accessing a
few data fields from the underlying Bot monad in which the main
application is operating.
Then again I want to be able to query a plugin's state from a
different plugin. For instance I'll have a plugin which keeps track of
the channels the bot has joined collecting user information, the topic,
etc. Another plugin could then query the "chan info" plugin and get all
the users in a certain channel through a queryPlugin function which
takes a plugin and looks that plugin up in the main application's
plugin state list for the right state and then calls a function on it.
The plugin and the corresponding functions would be exported by the
plugin's module.
queryPlugin :: (PluginClass a) => a -> (a -> b) -> PL m b
queryPlugin pl f = do
plugins <- getGlobalPlugins -- ideally (PluginClass a) => [a]
let pluginNames = map identifier plugins
targetName = identifier pl
[(_, target)] = filter ((==) targetName . fst) (zip pluginNames plugin)
return (f target)
But here I am facing either one or the other problem, depending on the "solution."
1) I somehow have to store all the plugin states in the main
application. Since plugins are essentially their states, they are quite
arbitrary. I either cannot use a list for that or I have to use
existential types which would make that possible.
2) Using an existential plugin type would restrict the functions I
am able to call on the plugin to those which are supported by the
PluginClass. This would render queryPlugin unusable since the functions
a plugin exports for query the state are arbitrary.
3) I could use Dynamics to store the plugin in a list *and* call
arbitrary functions, but then again how would I run a plugin? All the
main application know about the plugin state is that all the functions
defined by PluginClass are callable on the state. But the type
(PluginClass a) => a isn't enough to unwrap the Dynamic, apply the
function and wrap it again.
Another suggestion was to not use a class but make each plugin a
record, exporting the functions itself. Though I haven't given that a
serious thought, it seems not ideal to me. Using a class a plugin can
define the functions it actually uses. In my real code, they're about
12 functions the class exports and there could be more. Using a record
I would have to implement all possible functions even though they're
not changing the plugin's state nor causing side effects.
The most obvious solution would be to use an algebraic data type
with a constructor for each plugin. But I'd like to develop the plugins
independent from the core and I'd like to make them more dynamic than
that, maybe implementing dynamic loading of plugins at some time.
Then there are two other potential solutions, but I haven't looked
into them seriously since they seemed a little hackish to me at first
glance.
One would be using Hlist (which seems to be over-sized for
my problem as well) and another would be to store a tupel of (Dynamic,
(PluginClass a) => (Dynamic -> a)) as plugin state list where the
first element would be the plugin's state wrapped in a dynamic and the
second a unwrap function exported by the plugin's module. I *might* get
around the problem of being to unspecific about the type when
unwrapping, but this idea came only at the point of writing this email
and I would expect running into the same problems I have: It can be
achieved by either being too general or being to specific.
Hopefully I've explained everything well enough while not being too long with all this.
Thanks in advance for your help!