存在依赖于路径的类型,我认为可以在Scala中表达Epigram或Agda之类语言的几乎所有功能,但是我想知道为什么Scala不像在其他领域一样很好地支持这种语言(例如,DSLs)?我是否缺少诸如“没有必要”之类的东西?
存在依赖于路径的类型,我认为可以在Scala中表达Epigram或Agda之类语言的几乎所有功能,但是我想知道为什么Scala不像在其他领域一样很好地支持这种语言(例如,DSLs)?我是否缺少诸如“没有必要”之类的东西?
Answers:
除了语法上的便利之外,单例类型,路径依赖类型和隐式值的组合意味着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.type
和res3.type
是单例类型,因此比我们在RHS上验证的类型更精确)。
但是,实际上,在Scala中,我们不会先编码Sigma和Pi类型,然后再像在Agda或Idris中那样从那里开始。相反,我们将直接使用依赖于路径的类型,单例类型和隐式。您可以找到无数变形的无数示例:大小类型,可扩展记录,全面的HList,报废样板,通用Zipper等。
我只能看到的唯一反对意见是,在以上对Pi类型的编码中,我们要求依赖值的单例类型是可表示的。不幸的是,在Scala中,这仅适用于引用类型的值,而不适用于非引用类型的值(例如,Int)。这是一种耻辱,而不是固有的困难:Scala的类型检查代表内部的单类型的非参考值的,而且已经出现了夫妇的实验中让他们直接表达。在实践中,我们可以使用自然数的相当标准的类型级别编码来解决该问题。
无论如何,我认为这种轻微的域限制不能用作反对Scala作为依赖类型语言的地位的异议。如果是这样,那么对于依存ML(仅允许对自然数的依存关系)可以说是相同的,这是一个奇怪的结论。
我想这是因为(从经验中我知道,在Coq证明助手中使用了依赖类型,它可以完全支持它们,但仍然不是很方便的方式),依赖类型是非常高级的编程语言功能,确实很难正确-在实践中可能导致复杂性呈指数级增长。它们仍然是计算机科学研究的主题。
我相信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的所有值的属性的方程。与我们的Σ型一起,用于表示具有给定属性的对象的存在,形成了逻辑,其中我们的方程是一个需要证明的定理。
顺便说一句,在实际情况下,该定理可能是非常平凡的,直到无法自动从代码中导出定理或不花费大量精力即可解决。甚至可以以这种方式制定黎曼假说,只是发现没有实际证明就无法实现,永远循环或抛出异常的签名是不可能实现的。
Pi
用于根据值创建类型的示例。
depList
提取类型,为中的类型(非值)选择该类型。这种类型恰好是单例类型,目前可在Scala单例对象上使用并表示其确切值。示例为每个单例对象类型创建一个实现,从而将类型与值配对,如Σ-type。另一方面,--type是与其输入参数的结构匹配的公式。可能是Scala没有它们,因为Π型要求每个参数类型都是GADT,而Scala不会将GADT与其他类型区分开。U
Pi[T]
t
Pi
pi.U
在迈尔斯的例子算作依赖型?它的价值所在pi
。
pi.U
取决于的值pi
。阻止trait Pi[T]
成为Π型的问题是,如果不将其作为类型的提升,就不能使其依赖于任意参数的值(例如t
in depList
)。
问题是关于更直接地使用依赖类型的功能,我认为,采用一种比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太陌生,无法亲自回答这个问题。