简体   繁体   中英

A polymorphic render system in Haskell

I am trying to write a Markup language for my Haskell app which supports plugins. Plugin writers should not only be able to use it quickly but also be able to extends its functionality and create renderers themselves. That's why I created the class Renderable.

class Renderable a b where
  render :: a b -> b

To render an element you could do:

data SomeElement b = SomeElement ...

instance SomeElement SomeGUI where
  render = ...

You can also create elements which hold other elements:

data ListLayout b = ListLayout [b]

instance ListLayout SomeGUI where
  render = ...

In the end, you can render any (ab) to b as long as instances of Renderable ab exist:

let (myGUI :: b) = render (myLayout :: a b)

The problem arises when there are multiple instances of Renderable and I want to render the same value to multiple render outputs:

data SomeElement b = SomeElement

instance Renderable SomeElement GuiA
instance Renderable SomeElement GuiB

renderGuiA :: GuiA -> IO ()
renderGuiB :: GuiB -> IO ()

renderGuis layout = do
  renderGuiA (render layout)
  renderGuiB (render layout)

main :: IO ()
main = do
  let layout = SomeElement
  renderGuis layout

The compile infers the type of layout to be (a GuiA), since GuiA is the type renderGuiA expects. As a result, renderGuiB obviously won't compile since the types don't match. Similarly, trying to give renderGuis a type annotation does not work at all.

renderGuis :: (Renderable a GuiA, Renderable a GuiB) => a (GuiA or GuiB) -> IO ()

I was thinking of doing something like this:

renderGuis :: (Renderable a GuiA, Renderable a GuiB) => a ['GuiA, 'GuiB] -> IO ()

However, I do not really have the know-how and feel like I could run into a lot of other problems going down this road.

Can anyone think of a way to make this work without compromising functionality or extensibility? Any help would be greatly appreciated!

Your types have the type of thing that is to be rendered depend on the rendering context. So eg you might have a Button GTK which would be different from a Button HTML , for example. I think that is in most cases wrong. Here is an alternative way to do the typeclasses:

class Renderable a b where
  render :: a -> b -> b

Now you can still do things your old way if you like (eg instance Renderable (Button GTK) GTK ) although this requires language extensions.

Here is how one might now use this:

data HTML = HTML String

instance Renderable Label HTML where
  render (L text) (HTML pre) = HTML (pre ++ "<span>" ++ text ++ "</span>")

instance Renderable a HTML => Renderable (ListLayout a) HTML where
  render xs (HTML pre) = HTML (pre ++ "<div class=...>" ++ ((\(HTML x) -> x) <$> (\x -> render x (HTML "")) <$> xs) ++ "</div>")

Maybe a better class would be:

class Gui a where
  type Config a
  empty :: Config a -> a

class Gui b => Renderable a b where
  render :: a -> Config b -> b

While I now can do something like this,

main :: IO ()
main = do
  let a = render "" (testLayout :: R String) ++ "a"
      b = render (0 :: Int) (testLayout :: R Int) + 2
  return ()

i still cannot so the following, which was why I asked this question:

main :: IO ()
main = do
  let savedLayout = testLayout
  let a = render "" (savedLayout :: R String) ++ "a"
      b = render (0 :: Int) (savedLayout :: R Int) + 2
  return ()

I feel like achieving this will be much more difficult than anticipated. Furthermore, this is not a problem right now, but just a potential issue that could come up later. That's why I will close this question now and deal with this problem only when it is really necessary.

Edit: Rendering to String and Int is just for testing purposes and not what it is actually used for.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM