为什么我们需要总和类型?

时间:2016-11-15 22:32:34

标签: haskell type-theory

想象一种语言不允许数据类型的多个值构造函数。而不是写

 loginUser(localUser: LocalUser) {
        this.authSub = this.auth.loginUser(localUser)
            .subscribe(
            token => {
                this.token = token
            },
            error => {
                this.isLoggingIn = false;
                var errorObject = JSON.parse(error._body);
                this.errorMessage = errorObject.error_description;
                console.log(this.errorMessage);
                this.setLoggedIn({ email: "", password: "", error: this.errorMessage });

            },
            () => this.completeLogin(localUser));
    }

我们会有

data Color = White | Black | Blue

其中data White = White data Black = Black data Blue = Black type Color = White :|: Black :|: Blue (这里不是:|:以避免与sum类型混淆)是一个内置类型联合运算符。模式匹配可以以相同的方式工作

|

正如您所看到的,与副产品相比,它会产生扁平结构,因此您无需处理注射。并且,与sum类型不同,它允许随机组合类型,从而产生更大的灵活性和粒度:

show :: Color -> String
show White = "white"
show Black = "black"
show Blue = "blue"

我认为构建递归数据类型也不是问题

type ColorsStartingWithB = Black :|: Blue

我知道联合类型存在于TypeScript中,可能还有其他语言,但为什么Haskell委员会选择ADT而不是它们?

4 个答案:

答案 0 :(得分:12)

Haskell的总和类型与您的:|:非常相似。

两者之间的区别在于Haskell总和类型|标记的联合,而您的“总和类型”:|:是未标记的。

标记表示每个实例都是唯一的 - 您可以从Int | Int开始Int(实际上,这适用于任何a):

data EitherIntInt = Left Int | Right Int

在这种情况下:Either Int Int带有更多信息而不是Int,因为可能有LeftRight Int

:|:中,您无法区分这两者:

type EitherIntInt = Int :|: Int

你怎么知道它是左还是右Int

请参阅以下部分的扩展讨论的评论。

标记的联合具有另一个优点:编译器可以验证您是否作为程序员处理了所有情况,这对于一般未标记的联合是依赖于实现的。你在Int :|: Int处理了所有案件吗?根据定义,这与Int是同构的,或者编译器必须决定选择哪个Int(左或右),如果它们无法区分,这是不可能的。

考虑另一个例子:

type (Integral a, Num b) => IntegralOrNum a b = a :|: b    -- untagged
data (Integral a, Num b) => IntegralOrNum a b = Either a b -- tagged

未标记的联盟中5 :: IntegralOrNum Int Double是什么?它既是IntegralNum的实例,所以我们无法确定并且必须依赖实现细节。另一方面,标记的联合确切地知道5应该是什么,因为它标有LeftRight

至于命名:Haskell中的不相交联合是一个联合类型。 ADT只是实现这些目标的一种手段。

答案 1 :(得分:12)

我将尝试扩展@BenjaminHodgson提到的分类论证。

Haskell可以被视为类别Hask,其中对象是类型,而态射是类型之间的函数(忽略底部)。

我们可以将Hask中的产品定义为元组 - 明确地说它符合产品的定义:

  

ab的产品是c类型,其中包含pq投影,p :: c -> a和{{1}对于配备q :: c -> bc'的任何其他候选p',存在一个态射q',以便我们可以将m :: c' -> c写为p'p . m q'

product

Bartosz' Category Theory for Programmers中阅读此内容以获取更多信息。

现在对于每个类别,都存在相反的类别,它具有相同的态射,但会反转所有箭头。因此,副产品是:

  

q . mc的副产品ab类型,配备了ci :: a -> c注释,以便其他所有候选人j :: b -> c c'i' j'存在态射m :: c -> c'i' = m . ij' = m . j

coproduct

让我们看看标记和未标记的联合如何根据这个定义执行:

ab的未标记联盟是类型a :|: b,以便:

  • i :: a -> a :|: b定义为i a = a
  • j :: b -> a :|: b定义为j b = b

但是,我们知道a :|: aa同构。基于该观察,我们可以定义产品a :|: a :|: b的第二个候选者,其具有完全相同的态射。因此,没有单一的最佳候选者,因为ma :|: a :|: b之间的态射a :|: bidid是一个双射,暗示m是可逆的,并以任何方式“转换”类型。该论证的直观表示。将p替换为i,将q替换为j

coproduct untagged

限制自己Either,您可以通过以下方式验证自己:

  • i = Left
  • j = Right

这表明产品类型的分类补充是不相交的联合,而不是基于集合的联合。

set union是不相交联合的一部分,因为我们可以按如下方式定义它:

data Left a = Left a
data Right b = Right b
type DisjUnion a b = Left a :|: Right b

因为我们已经在上面说明了集合并不是两种类型的副产品的有效候选者,所以我们会丢失许多"free" properties(从 parametricity 后面提到的leftroundabout)不选择Hask类别中的不相交联盟(因为没有副产品)。

答案 2 :(得分:5)

这是一个我对自己有很多想法的想法:一种具有“一流类型代数”的语言。我们确信我们可以像Haskell那样做所有事情。当然,如果这些分离是像Haskell替代品那样标记联盟;然后你可以直接重写任何ADT来使用它们。实际上GHC可以为您做到这一点:如果您派生Generic实例,变体类型将由:+:构造表示,其实质上只是Either

我不确定未标记的工会是否也会这样做。只要您要求参与总和的类型明显不同,显式标记原则上不应该是必要的。然后,该语言需要一种方便的方法来在运行时匹配类型。听起来很像动态语言的作用 - 显然有很多开销。
最大的问题是,如果:|:两边的类型必须不相等,那么你将失去 parametricity ,这是Haskell最好的特征之一。 / p>

答案 3 :(得分:1)

鉴于您提到了TypeScript,了解its docs关于其联合类型的内容是有益的。那里的例子从一个函数开始......

function padLeft(value: string, padding: any) { //etc.

......有一个缺陷:

  

padLeft的问题是其填充参数的类型为any。这意味着我们可以使用既不是number也不是string

的参数来调用它

然后建议一个合理的解决方案,并拒绝:

  

在传统的面向对象代码中,我们可以通过创建类型层次结构来抽象这两种类型。虽然这更明确,但也有点矫枉过正。

相反,手册建议......

  

我们可以为any参数使用联合类型,而不是padding

function padLeft(value: string, padding: string | number) { // etc.

至关重要的是,联合类型的概念以这种方式描述:

  

联合类型描述的值可以是多种类型之一。

TypeScript中的string | number值可以是string类型,也可以是number类型,因为stringnumberstring | number的子类型(参见Alexis King对该问题的评论)。但是,Haskell中的Either String Int值既不是String类型也不是Int类型 - 它唯一的单态类型是Either String Int。在讨论的其余部分中出现了这种差异的进一步影响:

  

如果我们有一个具有联合类型的值,我们只能访问联合中所有类型共有的成员。

在一个大致类似的Haskell场景中,如果我们有Either Double Int,我们就不能直接在其上应用(2*),即使DoubleInt都有Num的实例。相反,bimap之类的东西是必要的。

  

当我们需要具体了解我们是否有Fish时会发生什么? [...]我们需要使用类型断言:

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

这种向下转换/运行时类型检查是at odds,其中Haskell类型系统通常如何工作,即使can be implemented使用相同类型的系统(也参见左下角的回答) )。相比之下,没有什么可以在运行时弄清楚Either Fish Bird的类型:案例分析发生在价值水平,并且没有必要处理任何失败和产生Nothing(或更糟糕的) ,null)由于运行时类型不匹配而导致。

相关问题