How can I Handle user plugins in my types?
Asked Answered
S

1

2

I'm writing a modular and extensible text editor in Haskell and I'd like to implement plugins in such a way: The writer of the plugin provides a single function that looks something like this:

handleEvent :: (PluginState, EditorState) -> Event -> (PluginState, EditorState)

As each event occurs the plugin can use the current editor state and customized chunk of its own state to calculate a new editor state and new plugin state. Of course each Plugin is going to have a different Type for the Plugin state, so I'm getting stuck on how I can integrate this into my system in a general way.

How can I write something vaguely like this:

type Plugin = (PluginState, EditorState) -> Event -> (PluginState, EditorState)
data MyEditor = MyEditor EditorState [Plugin] [PluginState]

When PluginState isn't a concrete type?

TLDR; How can I store a map of values with non-concrete types in an accessible way without baking in every plugin's state-type into my global state? I'm okay with re-compiling the editor when a new plugin is added.

Thanks! I'm really stuck on this one :/

If you need any clarification please just ask!

Staceestacey answered 19/11, 2016 at 22:2 Comment(2)
A starting point for this would be to look up how GHC handles plugins and how Yi handles configuration. Both take a partial recompile approach I think...Giga
Sounds like a case for hackage.haskell.org/package/vaultTranscendentalistic
I
2

Of course each Plugin is going to have a different Type for the Plugin state, so I'm getting stuck on how I can integrate this into my system in a general way.

Perhaps you could use an existential type to hide the plugin state, something like

{-# LANGUAGE ExistentialQuantification #-}

data Plugin = forall ps. Plugin { 
       currentState :: ps
    ,  transition :: ps -> EditorState -> Event -> (ps, EditorState) 
    }

handleEvent :: Plugin -> EditorState -> Event -> (Plugin,EditorState)
handleEvent (Plugin ps t) es e =
    let (ps',es') = t ps es e
    in  (Plugin ps' t,es')

Now each plugin is of the same type, and yet different plugin values can have internal states of different types:

charPlugin :: Plugin
charPlugin = Plugin 'a' (\ps es e -> (succ ps,es))

intPlugin :: Plugin
intPlugin = Plugin (1::Int) (\ps es e -> (succ ps,es))

(I took inspiration from the Fold type from the foldl package, which uses existentials in a similar way.)

You can now have a list of plugins:

plugins :: [Plugin]
plugins = [charPlugin,intPlugin]

A possible evolution of the design would be to constrain the internal states to be instances of some typeclass:

data Plugin = forall ps. Show ps => Plugin { 
       currentState :: ps
    ,  transition :: ps -> EditorState -> Event -> (ps, EditorState) 
    }

I suspect a Monoid instance could be defined for the Plugin type.

Also, we could think of explicitly parameterizing Plugin on the type of events it accepts, like

data Plugin e = ...

In that case Plugin could be made an instance of Contravariant and perhaps Divisible as well.

And if we go wild and parameterize on the editor state

data Plugin es e = ...

then perhaps we could find a way to "zoom" a given plugin to work in a more general state than the one for which it was defined.

Illiteracy answered 20/11, 2016 at 8:38 Comment(2)
It's a bit awkward to work with when applying (I have to unpack the transition function, apply it, then repack the state AND the transition function back up into a new Plugin to be returned), but aside from that it works like a charm! Thanks!Staceestacey
@Chris Penner The packing-unpacking can be handled by a single function like handleEvent, though. Also, the Plugin type should have strict fields to avoid space leaks.Illiteracy

© 2022 - 2024 — McMap. All rights reserved.