我发现自己在设计中遇到了相同的模式,我从一个具有几个数据构造函数的类型开始,最终希望能够针对这些数据构造函数键入,从而将它们拆分成它们自己的类型,然后在我仍然需要表示多个这些类型(即集合)的情况下,必须使用其中一个或另一个标记联合来增加程序其他部分的冗长。
我希望有人能给我指点一个更好的方法来完成我想要做的事情。让我从一个简单的例子开始。我正在对一个测试系统进行建模,在这个系统中,您可以拥有嵌套的测试套件,这些测试套件最终会以测试结束。所以,就像这样:
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] }
或者,我也可以使用“自定义”(特别是如果示例比较复杂并且有两个以上的选项),比如:
data Node =
fromTest Test
fromSuite Suite
因此,非常不幸的是,仅仅是为了能够拥有一个组合了Suite或Test的Suite
,我最终得到了一个奇怪的开销Either
类(无论它是具有实际的Either
还是自定义的类)。如果我使用存在类型类,我可以让Test
和Suite
都派生"Node_“,然后让Suite
拥有一个所说的Node
的列表。联产品将允许类似的东西,我基本上可以在没有冗长标签的情况下执行相同的Either
策略。
现在请允许我用一个更复杂的例子来扩展。测试的结果可以被跳过(测试被禁用)、成功、失败或省略(由于先前的失败而无法运行测试或套件)。再说一次,我最初是这样开始的:
data Result = Success | Omitted | Failure | Skipped
data ResultTree =
Tree { children::[ResultTree], result::Result } |
Leaf Result
但我很快意识到,我希望能够编写具有特定结果的函数,更重要的是,让类型本身强制执行所有权属性:一个成功的套件只能拥有成功或跳过的子代,失败的子代可以是任何东西,Omitted只能拥有Omitted,等等。所以现在我得到了这样的结果:
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
,但是,我得到了一些过去只能在运行时操作中强制执行的类型强制。有没有更好的策略来解决这个问题,而不需要打开像存在类型类这样的特性?
发布于 2018-10-26 02:05:51
我想你可能要找的是带有DataKinds
的GADTs
。这使您可以将数据类型中每个构造函数的类型细化为一组特定的可能值。例如:
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
,当两个类型a
和b
相等时,该类型由单个构造函数Refl
驻留;您可以对此进行模式匹配,以告知类型检查器类型相等,或者使用各种组合器执行更复杂的证明。
与GADTs
(和ExistentialTypes
,一般情况下)结合使用时也很有用的是RankNTypes
,它基本上允许您将多态函数作为参数传递给其他函数;如果您想要泛型地使用存在式函数,这是必要的:
consumeResult :: SomeResult -> (forall t. Result t -> r) -> r
consumeResult (SomeResult res) k = k res
这是延续传递样式(CPS)的一个示例,其中k
是延续。
最后要注意的是,这些扩展被广泛使用,并且基本上没有争议;当它们让您更直接地表达自己的意思时,您不必担心选择(大多数)类型的系统扩展。
https://stackoverflow.com/questions/52973009
复制相似问题