Haskell не использует более конкретный экземпляр класса типов - PullRequest
0 голосов
/ 04 января 2019

В последние несколько дней у меня были проблемы с выяснением, возможно ли что-то, что я пытаюсь сделать, на Хаскеле.

Вот некоторый контекст: Я пытаюсь написать небольшой язык разметки (похожий на 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 ++ ")"

-- ...

1 Ответ

0 голосов
/ 04 января 2019

Ваш код не компилируется, поскольку Block something имеет тип DocumentBlock, тогда как write ожидает аргумент Args a, и эти два типа различаются.Вы имели в виду writeBlock вместо этого?Я предполагаю, что так.

Возможно, вы захотите добавить ограничение в свой экзистенциальный тип, например:

data DocumentBlock
    = Paragraph String
    | forall a. StringWriter a => Block (Args a)
             -- ^^^^^^^^^^^^^^ --

Это имеет следующий эффект.Оперативно, каждый раз, когда используется Block something, экземпляр запоминается (указатель неявно сохраняется вдоль значения Args a).Это будет указатель на экземпляр catch-all или на конкретный, в зависимости от того, что лучше всего подходит.

Когда конструктор затем сопоставляется с шаблоном позже, экземпляр можно использовать.Полный рабочий код:

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}

-- Arguments for custom directives.
data family Args :: * -> *

data DocumentBlock
    = Paragraph String
    | forall a. StringWriter 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")
    ]

class StringWriter a where
    write :: Args a -> String

-- User defined generic conversion for all directives.
instance {-# OVERLAPPABLE #-} 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"))

Это печатает Math(a + b).

Последнее замечание: чтобы это работало, важно, чтобы все соответствующие экземпляры находились в области действия при использовании Block,В противном случае GHC может выбрать неправильный экземпляр, что приведет к непреднамеренному выводу.Это основное ограничение, делающее перекрывающиеся экземпляры в целом немного хрупкими.Пока нет ни одного экземпляра-сироты, это должно работать.

Также обратите внимание, что при использовании других экзистенциальных типов пользователь может (намеренно или случайно) заставить GHC в любом случае выбрать неправильный экземпляр.Например, если мы используем

data SomeArgs = forall a. SomeArgs (Args a)

toGenericInstance :: DocumentBlock -> DocumentBlock
toGenericInstance (Block a) = case SomeArgs a of
   SomeArgs a' -> Block a'  -- this will always pick the generic instance
toGenericInstance db = db

, тогда writeBlock (toGenericInstance (Block (MathArgs "a + b"))) выдаст Directive.

...