Я сталкиваюсь с тем же шаблоном в моих проектах, где я начинаю с типа с несколькими конструкторами данных, в конце концов хочу иметь возможность печатать на этих конструкторах данных и таким образом разбивать их на их собственные типы, только тогданеобходимо увеличить многословность других частей программы за счет необходимости использования либо одного, либо другого тегового объединения для ситуаций, когда мне все еще нужно представлять несколько таких типов (а именно, коллекции).
Я надеюсь, что кто-то может указатьмне лучший способ выполнить то, что я пытаюсь сделать.Позвольте мне начать с простого примера.Я моделирую систему тестирования, где вы можете иметь вложенные наборы тестов, которые в конечном итоге заканчиваются тестами.Итак, что-то вроде этого:
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
, но я получаю принудительное использование типов для чего-то, что раньше применялось только во время выполнения.Есть ли лучшая стратегия для этого, которая не включает в себя такие функции, как классы экзистенциальных типов?