我发现自己在我的设计中遇到了相同的模式,我从一个带有几个数据构造函数的类型开始,最终希望能够针对这些数据构造函数进行类型化,从而将它们拆分为自己的类型,然后必须增加在我仍然需要表示多个这些类型(即集合)的情况下,需要使用 Either 或另一个标记联合来降低程序其他部分的冗长性。
我希望有人可以为我指出一种更好的方法来完成我想做的事情。让我从一个简单的例子开始。我正在为一个测试系统建模,您可以在其中拥有最终以测试结束的嵌套测试套件。所以,像这样:
data Node =
Test { source::string }
Suite { title::string, children::[Node] }
所以,到目前为止非常简单,本质上是一个花哨的树/叶声明。但是,我很快意识到我希望能够制作专门用于测试的功能。因此,我现在将其拆分为:
data Test = Test { source::string }
data Suite = Suite { title::string, children::[Either Test Suite] }
或者,我可能会推出“自定义”Either(特别是如果示例更复杂并且有超过 2 个选项),例如:
data Node =
fromTest Test
fromSuite Suite
所以,已经很不幸了,仅仅为了能够拥有一个Suite
可以组合套件或测试的工具,我最终得到了一个奇怪的开销Either
类(无论是实际Either
的还是自定义的)。如果我使用存在类型类,我可以同时制作Test
并Suite
派生“Node_”,然后拥有Suite
一个上述Node
s 的列表。Coproducts 将允许类似的事情,我基本上会执行相同的Either
策略而没有标签的冗长。
现在让我用一个更复杂的例子来扩展。测试的结果可以是 Skipped(测试被禁用)、Success、Failure 或 Omitted(由于先前的失败,测试或套件无法运行)。同样,我最初是这样开始的:
data Result = Success | Omitted | Failure | Skipped
data ResultTree =
Tree { children::[ResultTree], result::Result } |
Leaf Result
但我很快意识到我希望能够编写获取特定结果的函数,更重要的是,让类型本身强制执行所有权属性:一个成功的套件必须只有成功或跳过的孩子,失败的孩子可以是任何东西,省略只能自己省略了,等等。所以现在我得到了这样的结果:
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
,但是,我得到了过去只能从运行时操作中强制执行的东西的类型强制。有没有更好的策略不涉及打开存在类型类之类的功能?