Haskell 存在类型类vs.数据构造函数vs.余积

Haskell 存在类型类vs.数据构造函数vs.余积,haskell,typeclass,existential-type,coproduct,Haskell,Typeclass,Existential Type,Coproduct,我发现自己在设计中遇到了相同的模式,我从一个带有几个数据构造函数的类型开始,最终希望能够针对这些数据构造函数进行键入,从而将它们划分为自己的类型,只是为了增加程序其他部分的冗长性,在我仍然需要表示多种类型(即集合)的情况下,需要使用一个或另一个标记的联合 我希望有人能给我指出一个更好的方法来完成我想做的事情。让我从一个简单的例子开始。我正在为一个测试系统建模,在这个系统中,您可以拥有嵌套的测试套件,最终以测试结束。比如说: data Node = Test { source::strin

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

我希望有人能给我指出一个更好的方法来完成我想做的事情。让我从一个简单的例子开始。我正在为一个测试系统建模,在这个系统中,您可以拥有嵌套的测试套件,最终以测试结束。比如说:

data Node =
    Test { source::string }
    Suite { title::string, children::[Node] }
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
到目前为止,非常简单,本质上是一个奇特的树/叶声明。然而,我很快意识到,我希望能够生成专门进行测试的函数。因此,我现在将其拆分为:

data Test = Test { source::string }
data Suite = Suite { title::string, children::[Either Test Suite] }
或者,我也可以滚动一个“自定义”(特别是如果示例更复杂且有2个以上选项),比如:

data Node =
   fromTest Test
   fromSuite Suite
因此,非常不幸的是,仅仅为了能够拥有一个可以组合成套件或测试的
套件,我就得到了一个奇怪的开销
类(无论是实际的
或自定义的
类)。如果我使用存在类型类,我可以使
Test
Suite
派生“节点”,然后让
Suite
拥有所述
节点的列表。副产品将允许类似的操作,在这里我基本上会执行相同的
策略,而不需要冗长的标记

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

data Node =
    Test { source::string }
    Suite { title::string, children::[Node] }
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
但我很快意识到,我希望能够编写具有特定结果的函数,更重要的是,让类型本身强制执行所有权属性:一个成功的套件必须只拥有成功或跳过的子项,失败的子项可以是任何东西,省略只能拥有省略,等等。因此,现在我得出这样的结论:

data Node =
    Test { source::string }
    Suite { title::string, children::[Node] }
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
,但是,我得到了一些过去只能从运行时操作中强制执行的类型强制。有没有一种更好的策略不需要打开诸如存在类型类之类的功能?

读到你的一句话时,我想到的第一件事是:“我很快意识到我想要能够编写具有特定结果的函数”是细化类型

它们只允许从类型中获取一些值作为输入,并使这些约束在编译时检查/出错

这段视频来自HaskellX 2018的一次演讲,介绍了LiquidHaskell,它允许在Haskell中使用细化类型:

您必须装饰haskell函数签名,并安装LiquidHaskell:

f::Int->i:Int{i|i<3}->Int
将是一个函数,它只能接受一个值为
<3
Int
作为第二个参数,并在编译时进行检查


您还可以对
结果
类型设置约束。

我认为您可能需要的是
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
然后,当函数仅在测试上运行时,它可以进行
节点的测试
;在套件上,一个
节点的套件
;在这两个节点上,都有一个多态的
节点a
。在
节点a上进行模式匹配时,每个
案例
分支都可以访问相等约束:

useNode :: Node a -> Foo
useNode node = case node of
  TestNode source ->          {- here it’s known that (a ~ 'Test) -}
  SuiteNode title children -> {- here, (a ~ 'Suite) -}
实际上,如果您进行了具体的
节点测试
,编译器将不允许使用
SuiteNode
分支,因为它无法匹配

SomeNode
是包装未知类型的
节点的存在式;如果需要,可以向其添加额外的类约束

您可以对
结果执行类似操作

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
中返回

为了处理动态结果,您可能需要向编译器证明两种类型相等;为此,我将您引导到,它提供了一个类型
a:~:b
,当两个类型
a
b
相等时,它由一个构造函数
Refl
居住;您可以在此基础上进行模式匹配,以通知类型检查器类型相等,或者使用各种组合器执行更复杂的证明

GADTs
(和
存在类型
,不太常见)结合使用的还有
RankNTypes
,它基本上使您能够将多态函数作为参数传递给其他函数;如果您想一般性地使用存在主义,这是必要的:

consumeResult :: SomeResult -> (forall t. Result t -> r) -> r
consumeResult (SomeResult res) k = k res
这是连续传递样式(CPS)的一个示例,其中
k
是连续

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

当然——我想我可以做到这一点(尽管我更想看看我是否只是不懂一些东西,我应该以不同的方式建模——理想情况下没有语言扩展)。我越是使用这些数据构造函数,它们就越有代码味道。就是