为什么scala不显式支持依赖类型?


109

存在依赖于路径的类型,我认为可以在Scala中表达Epigram或Agda之类语言的几乎所有功能,但是我想知道为什么Scala不像在其他领域一样很好地支持这种语言(例如,DSLs)?我是否缺少诸如“没有必要”之类的东西?


3
好吧,Scala的设计师相信Barendregt Lambda Cube并不是类型理论的全部。那可能是也可能不是原因。
约尔格W¯¯米塔格

8
@JörgWMittag什么是Lamda Cube?某种魔术装置?
Ashkan Kh。Nazary

@ ashy_32bit参见Barendregt的论文“通用类型系统简介”:diku.dk/hjemmesider/ansatte/henglein/papers/barendregt1991.pdf
iainmcgin 2012年

Answers:


151

除了语法上的便利之外,单例类型,路径依赖类型和隐式值的组合意味着Scala令人惊讶地对依赖类型提供了良好的支持,正如我尝试在shapeless中演示的那样。

Scala对依赖类型的内在支持是通过路径依赖类型。这些允许类型依赖于像这样通过对象(即值)图的选择器路径,

scala> class Foo { class Bar }
defined class Foo

scala> val foo1 = new Foo
foo1: Foo = Foo@24bc0658

scala> val foo2 = new Foo
foo2: Foo = Foo@6f7f757

scala> implicitly[foo1.Bar =:= foo1.Bar] // OK: equal types
res0: =:=[foo1.Bar,foo1.Bar] = <function1>

scala> implicitly[foo1.Bar =:= foo2.Bar] // Not OK: unequal types
<console>:11: error: Cannot prove that foo1.Bar =:= foo2.Bar.
              implicitly[foo1.Bar =:= foo2.Bar]

在我看来,以上内容足以回答“ Scala是依赖类型的语言吗?”的问题。肯定的是:很明显,这里我们有一些类型,这些类型的区别在于作为前缀的值。

但是,人们常常反对Scala不是“完全”依赖类型的语言,因为它没有像Agda或Coq或Idris那样的内在求和和乘积类型。我认为这在某种程度上反映了形式对基础的固定,但我将尝试表明Scala与通常认可的其他语言相比更加接近其他语言。

尽管有术语,但依存和类型(也称为Sigma类型)只是一对值,其中第二个值的类型取决于第一个值。这在Scala中可以直接表示,

scala> trait Sigma {
     |   val foo: Foo
     |   val bar: foo.Bar
     | }
defined trait Sigma

scala> val sigma = new Sigma {
     |   val foo = foo1
     |   val bar = new foo.Bar
     | }
sigma: java.lang.Object with Sigma{val bar: this.foo.Bar} = $anon$1@e3fabd8

实际上,这是依赖方法类型编码的关键部分,需要从 2.10之前(或更早通过实验性的-Ydependent-method类型Scala编译器选项)摆脱Scala中的“ Boomy of Doom”

从属产品类型(又名Pi类型)本质上是从值到类型的函数。它们是表示静态大小的向量以及依赖类型的编程语言的其他子代子的表示的关键。我们可以使用路径相关类型,单例类型和隐式参数的组合在Scala中对Pi类型进行编码。首先,我们定义一个特征,该特征将表示从类型T的值到类型U的函数,

scala> trait Pi[T] { type U }
defined trait Pi

然后我们可以定义使用这种类型的多态方法,

scala> def depList[T](t: T)(implicit pi: Pi[T]): List[pi.U] = Nil
depList: [T](t: T)(implicit pi: Pi[T])List[pi.U]

(请注意pi.U在结果类型中使用路径相关类型List[pi.U])。给定类型T的值,此函数将返回对应于该特定T值的(n个空)该类型的值的列表。

现在,让我们为要保留的功能关系定义一些合适的值和隐式见证,

scala> object Foo
defined module Foo

scala> object Bar
defined module Bar

scala> implicit val fooInt = new Pi[Foo.type] { type U = Int }
fooInt: java.lang.Object with Pi[Foo.type]{type U = Int} = $anon$1@60681a11

scala> implicit val barString = new Pi[Bar.type] { type U = String }
barString: java.lang.Object with Pi[Bar.type]{type U = String} = $anon$1@187602ae

现在这是我们使用Pi类型的功能,

scala> depList(Foo)
res2: List[fooInt.U] = List()

scala> depList(Bar)
res3: List[barString.U] = List()

scala> implicitly[res2.type <:< List[Int]]
res4: <:<[res2.type,List[Int]] = <function1>

scala> implicitly[res2.type <:< List[String]]
<console>:19: error: Cannot prove that res2.type <:< List[String].
              implicitly[res2.type <:< List[String]]
                    ^

scala> implicitly[res3.type <:< List[String]]
res6: <:<[res3.type,List[String]] = <function1>

scala> implicitly[res3.type <:< List[Int]]
<console>:19: error: Cannot prove that res3.type <:< List[Int].
              implicitly[res3.type <:< List[Int]]

(请注意,这里我们使用Scala的<:<subtype-witnessing运算符,而不是=:=因为res2.typeres3.type是单例类型,因此比我们在RHS上验证的类型更精确)。

但是,实际上,在Scala中,我们不会先编码Sigma和Pi类型,然后再像在Agda或Idris中那样从那里开始。相反,我们将直接使用依赖于路径的类型,单例类型和隐式。您可以找到无数变形的无数示例:大小类型可扩展记录全面的HList报废样板通用Zipper等。

我只能看到的唯一反对意见是,在以上对Pi类型的编码中,我们要求依赖值的单例类型是可表示的。不幸的是,在Scala中,这仅适用于引用类型的值,而不适用于非引用类型的值(例如,Int)。这是一种耻辱,而不是固有的困难:Scala的类型检查代表内部的单类型的非参考值的,而且已经出现了夫妇实验中让他们直接表达。在实践中,我们可以使用自然数相当标准的类型级别编码来解决该问题。

无论如何,我认为这种轻微的域限制不能用作反对Scala作为依赖类型语言的地位的异议。如果是这样,那么对于依存ML(仅允许对自然数的依存关系)可以说是相同的,这是一个奇怪的结论。


8
迈尔斯(Miles),感谢您提供非常详尽的答案。我对一件事有点好奇。您的示例似乎都不是乍看上去的,尤其是在Haskell中无法表达。那么您是否声称Haskell也是一种依赖类型的语言?
乔纳森·斯特林

8
我之所以投票,是因为我本质上无法将它们与McBride的“ Faking It” citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.22.2636中描述的技术区分开来-即这些是模拟方法依赖类型,而不直接提供它们。
sclv 2012年

2
@sclv我想您已经错过了Scala具有依赖类型而没有任何形式的编码的方法:请参见上面的第一个示例。没错,我对Pi类型的编码使用了一些与Connor的论文相同的技术,但是是从已经包含依赖于路径的类型和单例类型的衬底开始的。
Miles Sabin

4
不。当然,您可以将类型绑定到对象(这是对象作为模块的结果)。但是,如果不使用价值级别的见证者,就无法对这些类型进行计算。实际上=:=本身就是价值级别的见证者!您仍然在伪造它,就像您必须在Haskell中一样,或者甚至更多。
sclv 2012年

9
Scala的=:=不是值级的,它是类型构造函数-此处的值在这里:github.com/scala/scala/blob/v2.10.3/src/library/scala/…,并且似乎与以依赖类型的语言(例如,Agda和Idris)中的平等命题的见证人相比,尤其不同:refl。(分别分别参见www2.tcs.ifi.lmu.de/~abel/Equality.pdf第2节和eb.host.cs.st-andrews.ac.uk/writings/idris-tutorial.pdf第8.1节。)
pdxleif

6

我想这是因为(从经验中我知道,在Coq证明助手中使用了依赖类型,它可以完全支持它们,但仍然不是很方便的方式),依赖类型是非常高级的编程语言功能,确实很难正确-在实践中可能导致复杂性呈指数级增长。它们仍然是计算机科学研究的主题。


您足够友好地给我一些有关依赖类型的理论背景(也许是链接)吗?
Ashkan Kh。Nazary

3
@ ashy_32bit,如果您可以访问Benjamin Pierce的“类型和编程语言的高级主题”,则其中的一章将对依赖类型进行合理介绍。您还可以阅读Conor McBride的一些论文,他们对实践中的依赖类型特别是理论上的依赖类型特别感兴趣。
iainmcgin 2012年

3

我相信Scala的路径相关类型只能表示Σ类型,而不能表示Π类型。这个:

trait Pi[T] { type U }

不完全是Π型。根据定义,-型或从属乘积是一个函数,其结果类型取决于自变量值,表示通用量词,即∀x:A,B(x)。但是,在上述情况下,它仅取决于类型T,而不取决于此类型的某些值。Pi特征本身是一个Σ型,是一个存在量词,即∃x:A,B(x)。在这种情况下,对象的自引用充当量化变量。但是,当作为隐式参数传入时,由于它是按类型解析的,因此它简化为普通类型的函数。在Scala中对从属产品进行编码可能如下所示:

trait Sigma[T] {
  val x: T
  type U //can depend on x
}

// (t: T) => (∃ mapping(x, U), x == t) => (u: U); sadly, refinement won't compile
def pi[T](t: T)(implicit mapping: Sigma[T] { val x = t }): mapping.U 

这里缺少的一个功能是将字段x静态约束为期望值t的能力,从而有效地形成一个表示类型T的所有值的属性的方程。与我们的Σ型一起,用于表示具有给定属性的对象的存在,形成了逻辑,其中我们的方程是一个需要证明的定理。

顺便说一句,在实际情况下,该定理可能是非常平凡的,直到无法自动从代码中导出定理或不花费大量精力即可解决。甚至可以以这种方式制定黎曼假说,只是发现没有实际证明就无法实现,永远循环或抛出异常的签名是不可能实现的。


1
Miles Sabin上面显示了一个Pi用于根据值创建类型的示例。
missingfaktor

在示例中,从中depList提取类型,为中的类型(非值)选择该类型。这种类型恰好是单例类型,目前可在Scala单例对象上使用并表示其确切值。示例为每个单例对象类型创建一个实现,从而将类型与值配对,如Σ-type。另一方面,--type是与其输入参数的结构匹配的公式。可能是Scala没有它们,因为Π型要求每个参数类型都是GADT,而Scala不会将GADT与其他类型区分开。UPi[T]tPi
P. Frolov

好吧,我有点困惑。不会pi.U在迈尔斯的例子算作依赖型?它的价值所在pi
missingfaktor

2
它确实算作从属类型,但是它们有不同的风格:Σ型(“存在x使得P(x)”,从逻辑上来说)和Π型(“对于所有x,P(x)”而言) 。如您所述,类型pi.U取决于的值pi。阻止trait Pi[T]成为Π型的问题是,如果不将其作为类型的提升,就不能使其依赖于任意参数的值(例如tin depList)。
P. Frolov

1

问题是关于更直接地使用依赖类型的功能,我认为,采用一种比Scala提供的功能更直接的依赖类型的方法将是有益的。
当前的答案试图在类型理论层面上争论这个问题。我想对此进行更务实的讨论。这可以解释为什么在Scala语言中人们在依赖类型的支持级别上存在分歧。我们可能会想到一些不同的定义。(并不是说一个人是对的,一个人是错误的)。

这并不是试图回答将Scala变成Idris之类的东西(我想很难)或编写一个为Idris之类的功能提供更直接支持的库(例如singletons尝试在Haskell中)多么容易的问题。

相反,我想强调Scala与像Idris这样的语言之间的实用差异。
值和类型级别表达式的代码位是什么?Idris使用相同的代码,Scala使用非常不同的代码。

Scala(类似于Haskell)可能能够编码许多类型级别的计算。这由类似的库显示shapeless。这些库使用一些非常令人印象深刻且巧妙的技巧来做到这一点。但是,它们的类型级别代码(当前)与值级别表达式有很大的不同(我发现在Haskell中,差距会更小)。Idris允许在类型级别AS IS上使用值级别表达式。

显而易见的好处是代码重用(如果您需要在两个地方同时使用类型级表达式和值级,则无需分开编码)。编写价值级别代码应该更容易。不必像单例一样处理黑客(更不用说性能成本)了。您无需学习两件事,而只需学习一件事。在务实的层面上,我们最终需要更少的概念。输入同义词,类型族,函数,...函数呢?我认为,这种统一的好处要深得多,而不仅仅是语法上的便利。

考虑经过验证的代码。参见:https :
//github.com/idris-lang/Idris-dev/blob/v1.3.0/libs/contrib/Interfaces/Verified.idr
类型检查器验证monadic / functor /适用法律的证明,并且证明是关于实际的monad / functor / applicative的实现,而不是可能相同或不相同的某些编码类型级别等效项。最大的问题是我们证明了什么?

我可以使用聪明的编码技巧来完成相同的工作(对于Haskell版本,请参见以下内容,对于Scala,我还没有见过)
https://blog.jle.im/entry/verified-instances-in-haskell.html
https:// github.com/rpeszek/IdrisTddNotes/wiki/Play_FunctorLaws,
但类型是如此复杂,以至于很难看到规律,将值级别的表达式(自动但仍然)转换为类型级别的东西,并且您还需要信任该转换。所有这些都有错误的余地,这有点违反了编译器充当证明助手的目的。

(编辑2018.8.10)关于证明协助,这是Idris和Scala之间的另一个大区别。Scala(或Haskell)中没有什么可以阻止编写不同的证明:

case class Void(underlying: Nothing) extends AnyVal //should be uninhabited
def impossible() : Void = impossible()

而Idris具有total防止此类代码编译的关键字。

试图统一值和类型级别代码(例如Haskell singletons)的Scala库对于Scala对依赖类型的支持将是一个有趣的测试。由于依赖于路径的类型,这样的库能否在Scala中做得更好?

我对Scala太陌生,无法亲自回答这个问题。

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.