概述
类型级编程与传统的价值级编程有很多相似之处。然而,与计算发生在运行时的值级编程不同,在类型级编程中,计算发生在编译时。我将尝试在值级别的编程和类型级别的编程之间进行比较。
范式
类型级编程主要有两种范式:“面向对象”和“函数式”。从这里链接到的大多数示例都遵循面向对象的范例。
可以在 apocalisp 的 implementation of the lambda calculus 中找到一个很好的、相当简单的面向对象范式中的类型级编程示例,在此处复制:
// Abstract trait
trait Lambda {
type subst[U <: Lambda] <: Lambda
type apply[U <: Lambda] <: Lambda
type eval <: Lambda
}
// Implementations
trait App[S <: Lambda, T <: Lambda] extends Lambda {
type subst[U <: Lambda] = App[S#subst[U], T#subst[U]]
type apply[U] = Nothing
type eval = S#eval#apply[T]
}
trait Lam[T <: Lambda] extends Lambda {
type subst[U <: Lambda] = Lam[T]
type apply[U <: Lambda] = T#subst[U]#eval
type eval = Lam[T]
}
trait X extends Lambda {
type subst[U <: Lambda] = U
type apply[U] = Lambda
type eval = X
}
从示例中可以看出,类型级编程的面向对象范例如下:
- 首先:定义具有各种抽象类型字段的抽象特征(请参阅下文了解抽象字段是什么)。这是一个模板,用于保证某些类型字段存在于所有实现中而不强制实现。在 lambda 演算示例中,这对应于保证以下类型存在的
trait Lambda:subst、apply 和 eval。
- 下一步:定义扩展抽象特征并实现各种抽象类型字段的子特征
- 通常,这些子特征将使用参数进行参数化。在 lambda 演算示例中,子类型是
trait App extends Lambda,它使用两种类型参数化(S 和 T,两者都必须是 Lambda 的子类型),trait Lam extends Lambda 使用一种类型参数化(T),和trait X extends Lambda(未参数化)。
- 类型字段通常通过引用子特征的类型参数来实现,有时通过哈希运算符引用它们的类型字段:
#(与点运算符非常相似:. 用于值)。在 lambda 演算示例的特征 App 中,eval 类型实现如下:type eval = S#eval#apply[T]。这实际上是调用特征参数S 的eval 类型,并在结果上调用apply 参数T。注意,S 保证具有eval 类型,因为参数指定它是Lambda 的子类型。同样,eval 的结果必须具有apply 类型,因为它被指定为Lambda 的子类型,正如抽象特征Lambda 中所指定的那样。
Functional 范式由定义大量参数化类型构造函数组成,这些构造函数没有在特征中组合在一起。
值级编程与类型级编程的比较
-
abstract class
- 价值级别:
abstract class C { val x }
- 类型级别:
trait C { type X }
- 路径依赖类型
-
C.x(引用对象 C 中的字段值/函数 x)
-
C#x(在特征 C 中引用字段类型 x)
- 函数签名(未实现)
- 价值级别:
def f(x:X) : Y
- 类型级别:
type f[x <: X] <: Y(这称为“类型构造函数”,通常出现在抽象特征中)
- 功能实现
- 价值级别:
def f(x:X) : Y = x
- 类型级别:
type f[x <: X] = x
- 条件
- 检查相等性
- 价值级别:
a:A == b:B
- 类型级别:
implicitly[A =:= B]
- value-level:在 JVM 中通过运行时的单元测试发生(即没有运行时错误):
- 类型级别:通过类型检查在编译器中发生(即没有编译器错误):
- 本质上是一种类型比较:例如
implicitly[A =:= B]
-
A <:< B,仅当 A 是 B 的子类型时编译
-
A =:= B,仅当 A 是 B 的子类型且 B 是 A 的子类型时编译
-
A <%< B, ("viewable as") 仅当 A 可被视为 B 时才编译(即存在从 A 到 B 子类型的隐式转换)
- an example
- more comparison operators
类型和值之间的转换
诀窍是使用隐式函数和值。基本情况通常是隐式值,递归情况通常是隐式函数。事实上,类型级编程大量使用了隐式。
考虑这个例子(taken from metascala 和 apocalisp):
sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat
这里有自然数的 peano 编码。也就是说,每个非负整数都有一个类型:0 的特殊类型,即_0;每个大于零的整数都具有Succ[A] 形式的类型,其中A 是表示较小整数的类型。例如,表示 2 的类型将是:Succ[Succ[_0]](后继两次应用于表示 0 的类型)。
我们可以给各种自然数起别名,以便于参考。示例:
type _3 = Succ[Succ[Succ[_0]]]
(这很像将val 定义为函数的结果。)
现在,假设我们要定义一个值级函数def toInt[T <: Nat](v : T),它接受一个参数值v,它符合Nat,并返回一个整数,表示以v 编码的自然数类型。例如,如果我们有值val x:_3 = null(null 类型为Succ[Succ[Succ[_0]]]),我们希望toInt(x) 返回3。
要实现toInt,我们将使用以下类:
class TypeToValue[T, VT](value : VT) { def getValue() = value }
正如我们将在下面看到的,对于从 _0 到(例如)_3 的每个 Nat,将有一个从类 TypeToValue 构造的对象,并且每个对象都将存储相应类型的值表示(即TypeToValue[_0, Int] 将存储值0,TypeToValue[Succ[_0], Int] 将存储值1 等)。注意,TypeToValue 由两种类型参数化:T 和 VT。 T 对应于我们尝试分配值的类型(在我们的示例中,Nat),VT 对应于我们分配给它的值的类型(在我们的示例中,Int)。
现在我们进行以下两个隐式定义:
implicit val _0ToInt = new TypeToValue[_0, Int](0)
implicit def succToInt[P <: Nat](implicit v : TypeToValue[P, Int]) =
new TypeToValue[Succ[P], Int](1 + v.getValue())
我们实现toInt如下:
def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()
要了解toInt 的工作原理,让我们考虑一下它对几个输入的作用:
val z:_0 = null
val y:Succ[_0] = null
当我们调用toInt(z) 时,编译器会查找TypeToValue[_0, Int] 类型的隐式参数ttv(因为z 是_0 类型的)。它找到对象_0ToInt,调用该对象的getValue 方法并返回0。需要注意的重要一点是,我们没有向程序指定要使用哪个对象,编译器会隐式找到它。
现在让我们考虑toInt(y)。这一次,编译器查找TypeToValue[Succ[_0], Int] 类型的隐式参数ttv(因为y 的类型为Succ[_0])。它找到函数succToInt,该函数可以返回适当类型的对象(TypeToValue[Succ[_0], Int])并对其求值。此函数本身采用TypeToValue[_0, Int] 类型的隐式参数(v)(即,第一个类型参数为TypeToValue 的Succ[_] 少一个)。编译器提供_0ToInt(正如上面对toInt(z) 的评估所做的那样),succToInt 构造一个新的TypeToValue 对象,其值为1。同样,重要的是要注意编译器隐式提供所有这些值,因为我们无法显式访问它们。
检查您的工作
有多种方法可以验证您的类型级计算是否符合您的预期。这里有一些方法。制作两个类型A 和B,您要验证它们是否相等。然后检查以下是否编译:
-
Equal[A, B]
implicitly[A =:= B]
或者,您可以将类型转换为值(如上所示)并对值进行运行时检查。例如。 assert(toInt(a) == toInt(b)),其中a 的类型为A,b 的类型为B。
其他资源
完整的可用构造集可以在the scala reference manual (pdf) 的类型部分中找到。
Adriaan Moors 有几篇关于类型构造函数和相关主题的学术论文以及来自 scala 的示例:
Apocalisp 是一个博客,其中包含许多 scala 中类型级编程的示例。
ScalaZ 是一个非常活跃的项目,它提供了使用各种类型级编程特性扩展 Scala API 的功能。这是一个非常有趣的项目,拥有大量追随者。
MetaScala 是 Scala 的类型级库,包括自然数、布尔值、单位、HList 等元类型。它是Jesper Nordenberg (his blog) 的一个项目。
Michid (blog) 有一些很棒的 Scala 类型级编程示例(来自其他答案):
Debasish Ghosh (blog) 也有一些相关的帖子:
(我一直在对这个主题进行一些研究,这就是我所学到的。我还是新手,所以请指出这个答案中的任何不准确之处。)