首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >存在类型类vs.数据构造器vs. Coproducts

存在类型类vs.数据构造器vs. Coproducts
EN

Stack Overflow用户
提问于 2018-10-24 23:39:45
回答 1查看 191关注 0票数 4

我发现自己在设计中遇到了相同的模式,我从一个具有几个数据构造函数的类型开始,最终希望能够针对这些数据构造函数键入,从而将它们拆分成它们自己的类型,然后在我仍然需要表示多个这些类型(即集合)的情况下,必须使用其中一个或另一个标记联合来增加程序其他部分的冗长。

我希望有人能给我指点一个更好的方法来完成我想要做的事情。让我从一个简单的例子开始。我正在对一个测试系统进行建模,在这个系统中,您可以拥有嵌套的测试套件,这些测试套件最终会以测试结束。所以,就像这样:

代码语言:javascript
复制
data Node =
    Test { source::string }
    Suite { title::string, children::[Node] }

因此,到目前为止,非常简单,本质上是一个花哨的Tree/Leaf声明。然而,我很快意识到,我希望能够制作专门接受测试的函数。因此,我现在将其拆分如下:

代码语言:javascript
复制
data Test = Test { source::string }
data Suite = Suite { title::string, children::[Either Test Suite] }

或者,我也可以使用“自定义”(特别是如果示例比较复杂并且有两个以上的选项),比如:

代码语言:javascript
复制
data Node =
   fromTest Test
   fromSuite Suite

因此,非常不幸的是,仅仅是为了能够拥有一个组合了Suite或Test的Suite,我最终得到了一个奇怪的开销Either类(无论它是具有实际的Either还是自定义的类)。如果我使用存在类型类,我可以让TestSuite都派生"Node_“,然后让Suite拥有一个所说的Node的列表。联产品将允许类似的东西,我基本上可以在没有冗长标签的情况下执行相同的Either策略。

现在请允许我用一个更复杂的例子来扩展。测试的结果可以被跳过(测试被禁用)、成功、失败或省略(由于先前的失败而无法运行测试或套件)。再说一次,我最初是这样开始的:

代码语言:javascript
复制
data Result = Success | Omitted | Failure | Skipped
data ResultTree =
    Tree { children::[ResultTree], result::Result } |
    Leaf Result

但我很快意识到,我希望能够编写具有特定结果的函数,更重要的是,让类型本身强制执行所有权属性:一个成功的套件只能拥有成功或跳过的子代,失败的子代可以是任何东西,Omitted只能拥有Omitted,等等。所以现在我得到了这样的结果:

代码语言:javascript
复制
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,但是,我得到了一些过去只能在运行时操作中强制执行的类型强制。有没有更好的策略来解决这个问题,而不需要打开像存在类型类这样的特性?

EN

回答 1

Stack Overflow用户

发布于 2018-10-26 02:05:51

我想你可能要找的是带有DataKindsGADTs。这使您可以将数据类型中每个构造函数的类型细化为一组特定的可能值。例如:

代码语言:javascript
复制
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分支都会访问一个相等约束:

代码语言:javascript
复制
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 'TestSuiteNode分支将被编译器禁止,因为它永远不会匹配。

SomeNode是一个存在词,它包装了未知类型的Node;如果需要,您可以向其添加额外的类约束。

您可以使用Result做类似的事情

代码语言:javascript
复制
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,当两个类型ab相等时,该类型由单个构造函数Refl驻留;您可以对此进行模式匹配,以告知类型检查器类型相等,或者使用各种组合器执行更复杂的证明。

GADTs (和ExistentialTypes,一般情况下)结合使用时也很有用的是RankNTypes,它基本上允许您将多态函数作为参数传递给其他函数;如果您想要泛型地使用存在式函数,这是必要的:

代码语言:javascript
复制
consumeResult :: SomeResult -> (forall t. Result t -> r) -> r
consumeResult (SomeResult res) k = k res

这是延续传递样式(CPS)的一个示例,其中k是延续。

最后要注意的是,这些扩展被广泛使用,并且基本上没有争议;当它们让您更直接地表达自己的意思时,您不必担心选择(大多数)类型的系统扩展。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/52973009

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档