是否有理由在编程语言中使用底部类型?


49

底部类型是一种主要出现在数学类型理论中的构造。也称为空类型。它是没有值的类型,但是是所有类型的子类型。

如果函数的返回类型是底部类型,则表示它不返回。期。也许它永远循环,或者抛出异常。

在编程语言中使用这种奇怪的类型有什么意义?这种情况并不常见,但是它存在于某些环境中,例如Scala和Lisp。


2
@SargeBorsch:你确定吗?当然,不能用C显式定义void数据...
Basile Starynkevitch 2015年

3
@BasileStarynkevitch没有type的值void,并且单元类型必须有一个值。而且,正如您所指出的,您甚至不能声明type的值void,这意味着它甚至不是类型,只是该语言中的一种特殊情况。
Sarge Borsch

2
是的,C在这方面很奇怪,尤其是在如何编写指针和函数指针类型方面。但是void在Java中几乎是一样的:不是一个类型,也不能有值。
Sarge Borsch

3
在具有底部类型的语言的语义中,底部类型不被认为没有任何值,而是具有一个值(底部值),该值表示永远不会完成的计算。由于底部值是每种类型的值,因此底部类型可以是每种类型的子类型。
西奥多·诺韦尔

4
@BasileStarynkevitch Common Lisp的nil类型没有值。它还具有null类型,该类型只有一个值,即符号nil(aka,()),它是一种单位类型。
约书亚·泰勒

Answers:


33

我举一个简单的例子:C ++ vs Rust。

这是一个用于在C ++ 11中引发异常的函数:

[[noreturn]] void ThrowException(char const* message,
                                 char const* file,
                                 int line,
                                 char const* function);

这等效于Rust:

fn formatted_panic(message: &str, file: &str, line: isize, function: &str) -> !;

在纯粹的语法问题上,Rust构造更为明智。请注意,即使C ++构造也指定了不返回,但它仍指定了返回类型。有点奇怪

从标准上讲,C ++语法仅与C ++ 11一起出现(位于最前面),但是各种编译器已经提供了一段时间的各种扩展,因此必须对第三方分析工具进行编程以识别各种方式​​。该属性可以被写入。对其进行标准化显然显然是优越的。


现在,至于好处呢?

函数不返回的事实可用于:

  • 优化:可以修剪任何代码(不会返回),无需保存寄存器(因为无需恢复它们),...
  • 静态分析:消除了许多潜在的执行路径
  • 可维护性:(请参阅静态分析,但要人工操作)

6
void在您的C ++示例中定义了函数的类型(的一部分),而不是返回类型。它确实限制了该功能允许的值return;任何可以转换为空的东西(什么都没有)。如果函数return是s,则不能在其后跟随值。该函数的完整类型为void () (char const*, char const*, int, char const *)。+ 1用于char const代替const char:-)
清晰的时间2015年

4
但这并不意味着拥有底部类型更有意义,而只是在对函数是否作为语言一部分返回进行注释时才有意义。实际上,由于函数可能由于不同的原因而无法返回,因此似乎最好以某种方式对原因进行编码,而不是使用“包罗万象”的术语,就像相对较新的基于函数副作用的注释概念。
GregRos

2
实际上,有一个使“不返回”和“具有返回类型X”独立的原因:您自己代码的向后兼容性,因为调用约定可能取决于返回类型。
Deduplicator

[[noreturn]] 面值的语法或添加功能的?
Zaibis 2015年

1
[续]总体而言,我只想说,关于advantages的优点的讨论必须定义什么才可以构成⊥的实现。我认为没有(a →⊥)≤(ab)的类型系统不是a的有用实现。因此,从这种意义上说,SysV x86-64 C ABI(除其他外)只是不允许实现⊥。
Alex Shpilkin

26

卡尔的答案很好。这是我认为没有其他人提到的其他用途。的类型

if E then A else B

应该是一种类型,其中包含类型为的A所有值和类型为的所有值B。如果类型BNothing,则if表达式的类型可以为类型A。我会经常声明一个例程

def unreachable( s:String ) : Nothing = throw new AssertionError("Unreachable "+s) 

表示无法达到代码。由于其类型为Nothingunreachable(s)因此现在可以在不影响结果类型的情况下以任何方式if(或更频繁地)switch使用。例如

 val colour : Colour := switch state of
         BLACK_TO_MOVE: BLACK
         WHITE_TO_MOVE: WHITE
         default: unreachable("Bad state")

Scala具有这种Nothing类型。

另一个用例Nothing(如卡尔的回答中所述)是List [Nothing],即列表的类型,其成员的类型均为Nothing。因此,它可以是空列表的类型。

Nothing使这些用例起作用的关键特性不是它没有值-尽管在Scala中,它没有值-而是它是所有其他类型的子类型。

假设您有一种语言,其中每种类型都包含相同的值-称之为它()。用这种语言,具有()唯一价值的单位类型可以是每种类型的子类型。从OP的意义上来说,这并不能使其成为底层类型。OP很清楚,底部类型不包含任何值。但是,由于它是每个类型的子类型,因此它可以与底部类型扮演几乎相同的角色。

Haskell的处理方式有所不同。在Haskell中,从不产生值的表达式可以具有scheme类型forall a.a。这种类型方案的实例将与任何其他类型统一,因此即使(标准)Haskell没有子类型的概念,它也可以有效地作为底部类型。例如,error标准序言中的函数具有type scheme forall a. [Char] -> a。所以你可以写

if E then A else error ""

A对于任何表达式,表达式的类型将与的类型相同A

Haskell中的空列表具有类型方案forall a. [a]。如果A是类型为列表类型的表达式,则

if E then A else []

是与类型相同的表达式A


类型forall a . [a][a]Haskell中的类型有什么区别?Haskell类型表达式中的类型变量不是已经被普遍量化吗?
Giorgio 2015年

@Giorgio在Haskell中,如果很清楚您正在查看类型方案,则通用量化是隐式的。您甚至无法forall在标准的Haskell 2010中编写代码。我明确地编写了量化说明,因为这不是Haskell论坛,并且某些人可能不熟悉Haskell的约定。因此没有区别,除了forall a . [a]不是标准而相反[a]
西奥多·诺韦尔

19

类型以两种方式形成一个单面体,一起构成一个。这就是所谓的代数数据类型。对于有限类型,此半环与自然数的半环(包括零)直接相关,这意味着您要计算该类型具有多少个可能的值(不包括“非终止值”)。

  • 底部类型(我将其称为Vacuous)具有零值
  • 单位类型具有一个值。我将同时调用类型及其单个值()
  • 合成(大多数编程语言通过带有公共字段的记录/结构/类直接支持合成)是一种产品操作。举例来说,(Bool, Bool)有四个可能的值,即(False,False)(False,True)(True,False)(True,True)
    单元类型是合成操作的标识元素。例如((), False)((), True)是type的唯一值((), Bool),因此该类型与其Bool自身同构。
  • 在大多数语言中,替代类型在某种程度上都被忽略了(OO语言有点支持继承),但是它们的作用也一样。两种类型之间的替代AB基本上具有的所有值A加上的所有值B,因此为总和类型。举例来说,Either () Bool有三个值,我会打电话给他们Left ()Right FalseRight True
    最下面的类型是sum的identity元素:Either Vacuous A具有形式的值,因为没有意义(没有值)。Right aLeft ...Vacuous

这些monoid的有趣之处在于,当您在语言中引入函数时,这些类型的类别以及以函数为形态的函数就是一个monoidal类别。除其他外,这使您可以定义应用函子和monad,这对于使用纯函数项的通用计算(可能涉及副作用等)来说是极好的抽象。

现在,实际上,您只需要担心问题的一侧(合成Monoid)就可以解决很多问题,那么您实际上并不需要明确地使用底部类型。例如,即使Haskell长期以来也没有标准的底部类型。现在,它叫做Void

但是,当您将全图视为双笛卡尔封闭类别时,类型系统实际上等效于整个lambda演算,因此,基本上,您可以完美地抽象出图灵完备语言中的所有可能。非常适合嵌入式领域特定语言,例如,有一个有关以这种方式直接对电子电路进行编码项目

当然,您可能会说这是所有理论家的普遍废话。成为一名优秀的程序员,您根本不需要了解类别理论,但是当您这样做时,它就为您提供了强大而荒谬的通用方法来推理代码并证明不变式。


mb21提醒我注意,请勿将此与底值混淆。在像Haskell这样的惰性语言中,每种类型都包含一个表示为的底部“值” 。这不是您可以显式传递的具体内容,而是例如函数永久循环时“返回的”内容。甚至Haskell的Void类型也“包含”底部值,因此也包含名称。因此,Haskell的底部类型确实具有一个值,而其单位类型具有两个值,但是在类别理论讨论中,通常将其忽略。


“底部类型(我将其称为Void)”,不要与value 混淆,该bottomHaskell中任何类型的成员。
mb21

18

也许它永远循环,或者抛出异常。

在这种情况下,听起来像是一种有用的类型,尽管可能很少。

而且,即使Nothing(Scala的底部类型名称)可以没有值,List[Nothing]也没有该限制,这使其可用作空列表的类型。大多数语言通过将空字符串列表与整数空列表作为不同类型来解决此问题,这种方式是有意义的,但会使空列表更冗长地编写,这在面向列表的语言中是一个很大的缺点。


12
“ Haskell的空列表是一个类型构造函数”:当然,与此相关的更多的是它是多态的重载的 -也就是说,来自不同类型的空列表是不同的值,但是[]代表了所有值,并将被实例化为必要时的具体类型。
Peter LeFanu Lumsdaine

有趣的是:如果您尝试在Haskell解释器中创建一个空数组,则会得到一个非常确定的值,该类型具有非常不确定的类型:[a]。同样,:t Left 1yields Num a => Either a b。实际上评估表达式会强制使用以下类型a,但不是bEither Integer b
John Dvorak

5
空列表是一个构造函数。有点令人困惑,所涉及的类型构造函数具有相同的名称,但空列表本身是一个值而不是类型(嗯,也有类型级别列表,但这是另一个主题)。使空列表适用于任何列表类型的部分隐含forall在其类型中forall a. [a]。有一些不错的思考方法forall,但要弄清楚确实需要一些时间。
戴维(David)

@PeterLeFanuLumsdaine这就是类型构造函数的确切含义。这只是意味着它是一种与有所不同的类型*
GregRos

2
在Haskell []中,类型构造函数[]是一个表示空列表的表达式。但这并不意味着“ Haskell的空列表是类型构造函数”。上下文清楚表明[]是将其用作类型还是表达式。假设你声明data Foo x = Foo | Bar x (Foo x); 现在您可以Foo用作类型构造函数或值,但是碰巧您为两者选择了相同的名称。
Theodore Norvell

3

对于静态分析而言,记录特定代码路径不可访问的事实非常有用。例如,如果您在C#中编写以下代码:

int F(int arg) {
 if (arg != 0)
  return arg + 1; //some computation
 else
  Assert(false); //this throws but the compiler does not know that
}
void Assert(bool cond) { if (!cond) throw ...; }

编译器会抱怨F在至少一个代码路径中未返回任何内容。如果Assert将其标记为不可返回,则编译器将无需发出警告。


2

在某些语言中,null具有底部类型,因为所有类型的子类型都很好地定义了语言将null用作什么(尽管null既是自身又是返回自身的函数存在轻微的矛盾,避免了为何bot不宜居住的常见论点)。

它也可以用作函数类型(any -> bot)中的全部内容,以处理出错的调度。

并且某些语言允许您实际解决bot错误,可用于提供自定义编译器错误。


11
不,底部类型不是单位类型。底部类型根本没有任何值,因此返回底部类型的函数不应返回(即,引发异常或无限期地循环)
Basile Starynkevitch

@BasileStarynkevitch-我不是在谈论单位类型。单元类型映射到void通用语言(尽管对于相同的用途语义略有不同),但不是null。尽管您也说对了,但大多数语言不会将null建模为底部类型。
Telastyn

3
@TheodoreNorvell- Tangent的早期版本做到了-尽管我是作者,所以这可能是作弊。我没有为他人保存链接,自从进行这项研究以来已经有一段时间了。
Telastyn

1
@Martijn但是您可以使用null,例如,您将一个指针与null一个布尔值进行比较。我认为答案表明存在两种不同的底部类型。(a)语言(例如Scala),其中作为每种类型的子类型的类型表示不提供任何结果的计算。本质上,它是一个空类型,尽管从技术上讲,它通常由代表无终止的无用底值填充。(b)诸如Tangent之类的语言,其底部类型是其他所有类型的子集,因为它包含一个在其他所有类型中都可以找到的有用值-null。
Theodore Norvell

4
有趣的是,某些语言具有无法声明的类型的值(对于空文字来说很常见),而其他语言具有可以声明但没有值的类型(传统的底部类型),并且它们在某种程度上具有可比性。
Martijn 2015年

1

是的,这是一种非常有用的类型;虽然它的角色主要是类型系统的内部角色,但在某些情况下,底部类型会公开出现。

考虑以条件条件为表达式的静态类型语言(因此if-then-else构造是C和friends 的三元运算符的两倍,并且可能会有类似的多路case语句)。函数式编程语言具有此功能,但是它也发生在某些命令式语言中(自ALGOL 60起)。然后,所有分支表达式都必须最终产生整个条件表达式的类型。可以简单地要求它们的类型相等(我认为C中的三元运算符就是这种情况),但是这种限制过于严格,特别是当条件也可以用作条件语句(不返回任何有用的值)时。通常,人们希望每个分支表达式都是(隐式)可转换的 到将成为完整表达式类型的通用类型(可能具有或多或少的复杂限制,以允许编译器有效地找到通用类型,请参见C ++,但在此不再赘述)。

在两种情况下,通用类型的转换将允许这种条件表达式具有必要的灵活性。已经提到一种,结果类型是单位类型void; 这自然是所有其他类型的超类型,并且允许(平凡地)将任何类型转换为它,从而可以将条件表达式用作条件语句。另一种情况涉及表达式确实返回有用值,但是一个或多个分支无法产生一个值的情况。它们通常会引发异常或涉及跳转,并且要求它们(也)产生整个表达式的类型的值(从不可达的角度出发)将毫无意义。可以通过提供引发异常的子句,跳转和调用来优雅地处理这种情况,即底部类型,即可以(平凡地)转换为任何其他类型的一种类型。

我建议编写这样的底部类型,*以建议其可转换为任意类型。它可能在内部达到其他有用的目的,例如,当试图为没有声明任何递归的递归函数推导出结果类型时,类型推论者可以将该类型分配*给任何递归调用,以避免发生鸡与蛋的情况。实际类型将由非递归分支确定,并将递归分支转换为非递归分支的通用类型。如果根本没有非递归分支,则类型将保留*,并正确指示该函数没有从递归返回的任何可能方式。除此之外,作为异常抛出函数的结果类型,可以使用*作为长度为0的序列的组件类型,例如空列表;再次,如果从类型的表达式中选择了一个元素[*](可能为空列表),那么生成的类型*将正确指示该元素永远不会返回而不会出现错误。


那么,由于表达式永远不会产生其他任何东西,var foo = someCondition() ? functionReturningBar() : functionThatAlwaysThrows()是否可以推断fooas 的类型Bar呢?
2015年

1
您刚刚描述了单位类型-至少在答案的第一部分中已经进行了描述。返回单位类型功能是一样的其中一个声明为返回void的C.你的答案,在这里你谈论一个类型,它永远不会返回的函数,或列表没有元素-第二部分即确底部类型!(它通常写为_|_而不是*。不确定为什么。也许是因为它看起来像一个(人类)底部:)
andrewf 15/3/24

2
为了避免疑问:“不返回任何有用的东西”与“不返回任何东西”是不同的;第一个由单元类型表示;第二个按底部类型。
andrewf

@andrewf:是的,我明白区别。我的回答有点冗长,但我想指出的是,单元类型和底部类型在允许更灵活地使用某些表达式(但仍很安全)方面都起到了(不同但可比)的作用。
Marc van Leeuwen

@supercat:是的,就是这个主意。目前在C ++中是非法的,但如果它是有效的functionThatAlwaysThrows(),通过一个明确的替代throw,由于标准的特殊语言。具有执行此操作的类型将是一个改进。
Marc van Leeuwen

0

在某些语言中,您可以注释一个函数,以告知编译器和开发人员该函数的调用不会返回(并且如果该函数以可以返回的方式编写,则编译器将不允许它)。知道这很有用,但是最后您可以像其他任何函数一样调用这样的函数。编译器可以使用该信息进行优化,发出有关无效代码的警告,等等。因此,没有非常有说服力的理由拥有这种类型,但也没有非常有说服力的理由避免这种类型。

在许多语言中,函数可以返回“ void”。确切的含义取决于语言。在C语言中,这意味着该函数不返回任何内容。在Swift中,这意味着该函数返回的对象只有一个可能的值,并且由于只有一个可能的值,该值占用零位,并且实际上不需要任何代码。无论哪种情况,都与“底部”不同。

“底部”将是没有可能值的类型。它永远不会存在。如果函数返回“底部”,则它实际上无法返回,因为没有可以返回的“底部”类型的值。

如果语言设计师对此感到满意,那么就没有理由不使用这种类型。实现并不困难(您可以像返回void并标记为“不返回”的函数一样实现它)。您不能混合使用指向返回底部的函数的指针和指向返回void的函数的指针,因为它们不是同一类型)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.