Классы экзистенциальных типов против конструкторов данных и копроизведений - PullRequest
0 голосов
/ 24 октября 2018

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

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

data Node =
    Test { source::string }
    Suite { title::string, children::[Node] }

Итак, пока довольно просто, по сути, причудливое объявление Tree / Leaf.Тем не менее, я быстро понимаю, что хочу создавать функции, специально предназначенные для тестов.Таким образом, теперь я разделю это следующим образом:

data Test = Test { source::string }
data Suite = Suite { title::string, children::[Either Test Suite] }

В качестве альтернативы я мог бы бросить «пользовательский» Либо (особенно если пример более сложный и имеет более 2 вариантов), скажем что-то вроде:

data Node =
   fromTest Test
   fromSuite Suite

Итак, уже довольно прискорбно, что просто для того, чтобы иметь Suite, который может иметь комбинацию из комплектов или тестов, я получаю странный класс Either сверх накладных расходов (будь тобыть с фактическим Either или пользовательским).Если бы я использовал классы экзистенциальных типов, я мог бы сделать так, чтобы и Test, и Suite выводили "Node_", а затем Suite имели бы свой список упомянутых Node s.Копродукции позволили бы сделать что-то похожее, где я бы по сути применил ту же стратегию Either без многословия тегов.

Позвольте мне теперь перейти к более сложному примеру.Результаты тестов могут быть Пропущены (тест был отключен), Успешно, Сбой или Пропущен (тест или набор не могли быть выполнены из-за предыдущего сбоя).Опять же, я изначально начал с чего-то вроде этого:

data Result = Success | Omitted | Failure | Skipped
data ResultTree =
    Tree { children::[ResultTree], result::Result } |
    Leaf Result

Но я быстро понял, что хочу иметь возможность писать функции, которые получают конкретные результаты, и, что более важно, иметь сам тип, обеспечивающий свойства владения: AУспешная сюита должна иметь только Успешных или Пропущенных детей, дети Failure могут быть чем угодно, Пропущенные могут иметь только собственные Пропущенные и т. д. Итак, теперь я получаю что-то вроде этого:

data Success = Success { children::[Either Success Skipped] }
data Failure = Failure { children::[AnyResult] }
data Omitted = Omitted { children::[Omitted] }
data Skipped = Skipped { children::[Skipped] }
data AnyResult =
  fromSuccess Success |
  fromFailure Failure |
  fromOmitted Omitted |
  fromSkipped Skipped

Опять, у меня теперь есть эти странныеТипы «обертки», такие как AnyResult, но я получаю принудительное использование типов для чего-то, что раньше применялось только во время выполнения.Есть ли лучшая стратегия для этого, которая не включает в себя такие функции, как классы экзистенциальных типов?

Ответы [ 2 ]

0 голосов
/ 25 октября 2018

Я думаю, что вы можете искать GADTs с DataKinds.Это позволяет уточнить типы каждого конструктора в типе данных до определенного набора возможных значений.Например:

data TestType = Test | Suite

data Node (t :: TestType) where
  TestNode :: { source :: String } -> Node 'Test
  SuiteNode :: { title :: String, children :: [SomeNode] } -> Node 'Suite

data SomeNode where
  SomeNode :: Node t -> SomeNode

Тогда, когда функция работает только на тестах, она может принять Node 'Test;на номера люкс Node 'Suite;и на любой, полиморфный Node a.При сопоставлении с образцом в Node a каждая ветвь case получает доступ к ограничению равенства:

useNode :: Node a -> Foo
useNode node = case node of
  TestNode source ->          {- here it’s known that (a ~ 'Test) -}
  SuiteNode title children -> {- here, (a ~ 'Suite) -}

Действительно, если вы взяли конкретный Node 'Test, ветвь SuiteNode будет запрещенакомпилятор, поскольку он никогда не может совпадать.

SomeNode - это экзистенциал, заключающий в себе Node неизвестного типа;Вы можете добавить к этому дополнительные ограничения класса.

Вы можете сделать то же самое с Result:

data ResultType = Success | Omitted | Failure | Skipped

data Result (t :: ResultType) where
  SuccessResult
    :: [Either (Result 'Success) (Result 'Skipped)]
    -> Result 'Success
  FailureResult
    :: [SomeResult]
    -> Result 'Failure
  OmittedResult
    :: [Result 'Omitted]
    -> Result 'Omitted
  SkippedResult
    :: [Result 'Skipped]
    -> Result 'Skipped

data SomeResult where
  SomeResult :: Result t -> SomeResult

Конечно, я предполагаю, что в вашем реальном коде есть больше информации вэти типы;как это, они не представляют много.Если у вас есть динамическое вычисление, такое как запуск теста, который может давать разные результаты, вы можете вернуть его в виде SomeResult.

Чтобы работать с динамическими результатами, вам может потребоваться доказатькомпилятор, что два типа равны;для этого я направляю вас к Data.Type.Equality, который предоставляет тип a :~: b, который населяется одним конструктором Refl, когда два типа a и b равны;Вы можете сопоставить это с шаблоном, чтобы проинформировать проверщика типов о равенстве типов, или использовать различные комбинаторы для выполнения более сложных доказательств.

Также полезно в сочетании с GADTsExistentialTypes, реже в целом)is RankNTypes, что в основном позволяет передавать полиморфные функции в качестве аргументов другим функциям;это необходимо, если вы хотите использовать экзистенциальную сущность в общем:

consumeResult :: SomeResult -> (forall t. Result t -> r) -> r
consumeResult (SomeResult res) k = k res

Это пример стиля передачи продолжения (CPS), где k - продолжение.

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

0 голосов
/ 24 октября 2018

Первое, что пришло мне в голову при чтении вашего предложения: «Я быстро понял, что хочу написать функции, которые получают конкретные результаты», это Типы уточнения .

Они позволяютвзять только некоторые значения из типа в качестве входных данных и сделать эти ограничения проверкой / ошибкой во время компиляции.

Это видео из выступления на HaskellX 2018, в котором представлен LiquidHaskell, который позволяет использовать типы уточненияв Haskell:

https://skillsmatter.com/skillscasts/11068-keynote-looking-forward-to-niki-vazou-s-keynote-at-haskellx-2018

Вы должны украсить сигнатуру своей функции haskell и установить LiquidHaskell:

f :: Int -> i : Int {i | i < 3} -> Int будет функцией, которая может принимать только каквторой параметр Int со значением < 3, проверенный во время компиляции.

Вы также можете наложить ограничения на тип Result.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...