В последние несколько дней у меня были проблемы с выяснением, возможно ли что-то, что я пытаюсь сделать, на Хаскеле.
Вот некоторый контекст:
Я пытаюсь написать небольшой язык разметки (похожий на ReST), в котором синтаксис уже включает пользовательские расширения с помощью директив .
Чтобы пользователи могли реализовывать новые директивы, они должны иметь возможность добавлять новые семантические конструкции в тип данных документа. Например, если кто-то хочет добавить директиву для отображения математики, он может захотеть иметь конструктор MathBlock String
внутри ast.
Очевидно, что типы данных не являются расширяемыми, и решение, в котором есть универсальный конструктор DirectiveBlock String
, содержащий имя директивы (здесь "math"
), нежелательно, поскольку мы хотели бы, чтобы в нашем ast было только правильно сформированное конструкции (так что только директивы с правильно сформированными аргументами).
Используя семейства типов, я прототипировал что-то вроде:
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
-- Arguments for custom directives.
data family Args :: * -> *
data DocumentBlock
= Paragraph String
| forall a. Block (Args a)
Конечно, если кто-то хочет определить новую директиву для отображения математики, он может сделать это следующим образом:
data Math
-- The expected arguments for the math directive.
data instance Args Math = MathArgs String
doc :: [DocumentBlock]
doc =
[ Paragraph "some text"
, Block (MathArgs "x_{n+1} = x_{n} + 3")
]
Пока все хорошо, мы можем создавать документы только тогда, когда блоки директив получают правильные аргументы.
Проблема возникает, когда один пользователь хочет преобразовать внутреннее представление документа в какой-либо пользовательский вывод, скажем, String.
Пользователь должен предоставить вывод по умолчанию для всех директив , так как их будет много, и некоторые из них не могут быть преобразованы в цель.
Кроме того, пользователь может пожелать предоставить более конкретный вывод для некоторых директив :
class StringWriter a where
write :: Args a -> String
-- User defined generic conversion for all directives.
instance StringWriter a where
write _ = "Directive"
-- Custom way of showing the math directive.
instance StringWriter Math where
write (MathArgs raw) = "Math(" ++ raw ++ ")"
-- Then to display a DocumentBlock
writeBlock :: DocumentBlock -> String
writeBlock (Paragraph t) = "Paragraph(" ++ t ++ ")"
writeBlock (Block args) = write args
main :: IO ()
main = putStrLn $ writeBlock (Block (MathArgs "a + b"))
В этом примере выводом является Block
, а не Math(a+b)
, поэтому всегда выбирается универсальный экземпляр для StringWriter. Даже при игре с {-# OVERLAPPABLE #-}
ничего не получается.
Возможно ли вообще поведение, которое я описываю в Хаскеле?
При попытке включить универсальный Writer в определение Block
он также не компилируется.
-- ...
class Writer a o where
write :: Args a -> o
data DocumentBlock
= Paragraph String
| forall a o. Writer a o => Block (Args a)
instance {-# OVERLAPPABLE #-} Writer a String where
write _ = "Directive"
instance {-# OVERLAPS #-} Writer Math String where
write (MathArgs raw) = "Math(" ++ raw ++ ")"
-- ...