Полиморфная система рендеринга в Haskell - PullRequest
0 голосов
/ 15 декабря 2018

Я пытаюсь написать язык разметки для моего приложения на Haskell, которое поддерживает плагины.Авторы плагинов должны иметь возможность не только быстро использовать его, но и расширять его функциональность и сами создавать средства визуализации.Вот почему я создал класс 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 = ...

В конце концов, вы можете сделать любую (ab) для b, если существуют экземпляры Renderable ab:

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

Однако у меня нетНоу-хау и ощущение, что я мог бы столкнуться с множеством других проблем на этом пути.

Может кто-нибудь придумать, как сделать это без ущерба для функциональности или расширяемости?Любая помощь будет принята с благодарностью!

Ответы [ 2 ]

0 голосов
/ 16 декабря 2018

Хотя сейчас я могу сделать что-то подобное,

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 предназначен только для целей тестирования, а не для того, для чего он фактически используется.

0 голосов
/ 15 декабря 2018

Ваши типы имеют тип вещи, которая должна быть визуализирована, зависит от контекста визуализацииНапример, у вас может быть Button GTK, который, например, будет отличаться от Button HTML.Я думаю, что в большинстве случаев это неправильно.Вот альтернативный способ сделать классы типов:

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 Gui a where
  type Config a
  empty :: Config a -> a

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