有路径依赖类型,我认为可以在Scala中表达Epigram或Agda等语言的几乎所有功能,但我想知道为什么Scala不像它那样更明确地支持this在其他领域(比如DSL)非常好吗? 我缺少的任何东西都像“没必要”?
答案 0 :(得分:139)
除了语法上的便利之外,单例类型,路径依赖类型和隐式值的组合意味着Scala对依赖类型的支持非常好,正如我在shapeless中尝试演示的那样。
Scala对依赖类型的内在支持是通过path-dependent types。这些允许类型依赖于通过对象(即,值 - )图形的选择器路径,如此,
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那样的dependent sum and product types作为内在函数。我认为这在一定程度上反映了形式对基本面的影响,然而,我会试着证明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之前Scala中encoding of dependent method types which is needed to escape from the 'Bakery of Doom'的一个关键部分(或者更早通过实验-Ydependent方法类型Scala编译器选项)。
依赖产品类型(又名Pi类型)本质上是从值到类型的函数。它们是statically sized vectors和其他海报儿童代表依赖类型编程语言的关键。我们可以使用路径依赖类型,单例类型和隐式参数的组合在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-type-using功能,
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的<:<
子类型见证运算符而不是=:=
,因为res2.type
和res3.type
是单例类型,因此比我们的类型更精确验证RHS)。
然而,在实践中,在Scala中,我们不会从编码Sigma和Pi类型开始,然后像在Agda或Idris那样从那里开始。相反,我们将直接使用路径依赖类型,单例类型和隐含。你可以找到很多关于如何在无形状中发挥作用的例子:sized types,extensible records,comprehensive HLists,scrap your boilerplate,generic Zippers等等。
我能看到的唯一剩下的反对意见是,在Pi类型的上述编码中,我们要求依赖值的单例类型是可表达的。不幸的是,在Scala中,这仅适用于引用类型的值,而不适用于非引用类型的值(尤其是Int)。这是一个耻辱,但不是一个内在的困难:Scala的类型检查器在内部表示单引号类型的非引用值,并且couple experiments使它们可以直接表达。在实践中,我们可以使用fairly standard type-level encoding of the natural numbers解决问题。
在任何情况下,我都不认为这种轻微的域名限制可以用作对Scala作为依赖类型语言的状态的反对。如果是,那么对于Dependent ML(它只允许依赖于自然数值)也可以这样说,这将是一个奇怪的结论。
答案 1 :(得分:5)
我认为这是因为(根据我的经验,在Coq校对助手中使用了依赖类型,它完全支持它们但仍然不是非常方便)依赖类型是一种非常高级的编程语言特性,它是真的很难做到 - 并且可能导致实践中复杂性的指数爆炸。它们仍然是计算机科学研究的主题。
答案 2 :(得分:2)
我相信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的所有值的属性的等式。与我们的Σ-类型一起,用于表示给定物体的存在属性,逻辑形成,其中我们的方程是一个被证明的定理。
另一方面,在实际情况中,定理可能非常重要,直到它无法自动从代码中导出或无需大量努力即可解决。甚至可以用这种方式制定黎曼假设,只是发现签名不可能实现而不实际证明,永远循环或抛出异常。
答案 3 :(得分:0)
问题是关于更直接地使用依赖类型的功能,我认为,
与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
类型检查器验证单子/函数/适用法律的证明以及
证明是关于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
关键字阻止了此类代码的编译。
一个试图统一值和类型级别代码的Scala库(例如Haskell singletons
)对于Scala对依赖类型的支持将是一个有趣的测试。由于依赖于路径的类型,这样的库能否在Scala中做得更好?
对于Scala来说,我太新了,无法自己回答这个问题。