Haskell 中的多态渲染系统

A polymorphic render system in Haskell

我正在尝试为支持插件的 Haskell 应用编写标记语言。插件编写者不仅应该能够快速使用它,还应该能够扩展它的功能并自己创建渲染器。这就是我创建 class Renderable 的原因。

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

渲染一个元素你可以这样做:

data SomeElement b = SomeElement ...

instance SomeElement SomeGUI where
  render = ...

您还可以创建包含其他元素的元素:

data ListLayout b = ListLayout [b]

instance ListLayout SomeGUI where
  render = ...

最后,只要 Renderable a b 的实例存在,您就可以渲染任何 (a b) 到 b:

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

当有多个 Renderable 实例并且我想将相同的值渲染到多个渲染输出时,问题就出现了:

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

编译推断布局类型为(GuiA),因为 GuiA 是 renderGuiA 期望的类型。结果,renderGuiB 显然不会编译,因为类型不匹配。同样,尝试给 renderGuis 类型注释根本不起作用。

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

我正在考虑做这样的事情:

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

然而,我并没有真正的专业知识,我觉得我可以 运行 解决很多其他问题。

谁能想出一种方法来完成这项工作而不影响功能或可扩展性? 任何帮助将不胜感激!

您的类型具有要呈现的事物的类型,具体取决于呈现上下文。所以例如例如,您可能有一个 Button GTKButton HTML 不同。我认为在大多数情况下这是错误的。这是执行 typeclasses 的另一种方法:

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

现在,如果您愿意,您仍然可以按照原来的方式做事(例如 instance Renderable (Button GTK) GTK),尽管这需要语言扩展。

现在可以这样使用它:

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>")

也许更好的 class 是:

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

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

虽然我现在可以做这样的事情,

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

下面的我还是不行,这就是我问这个问题的原因:

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

我觉得实现这个目标比预想的要困难得多。此外,这不是现在的问题,而是以后可能会出现的潜在问题。这就是为什么我现在关闭这个问题,只有在真正需要的时候才处理这个问题。

编辑:渲染为 String 和 Int 只是为了测试目的,而不是它的实际用途。