Answers:
总览
类型级编程与传统的价值级编程有很多相似之处。但是,与值级编程不同,值级编程在运行时进行计算,而在类型级编程中,计算在编译时进行。我将尝试在值级别的编程和类型级别的编程之间取得相似之处。
范式
类型级编程中有两种主要范例:“面向对象”和“功能”。从此处链接的大多数示例都遵循面向对象的范例。
在apocalisp的lambda演算的实现中可以找到一个很好,相当简单的面向对象范例中的类型级编程示例,该示例在此处复制:
// 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
}
从该示例可以看出,用于类型级编程的面向对象范例如下:
trait Lambda该保证了以下类型的存在:subst,apply,和eval。trait App extends Lambda使用两种类型(S和T,都必须是的子类型Lambda)对子类型进行trait Lam extends Lambda参数化,使用一种类型(T)对参数进行子化,以及trait X extends Lambda(不进行参数化)。#与点运算符非常相似:.用于值)。在性状Applambda演算示例的,类型eval是这样实现的:type eval = S#eval#apply[T]。本质上,这是调用evaltrait参数的类型S,并在结果上apply使用parameter T进行调用。注意,S保证具有eval类型,因为参数将其指定为的子类型Lambda。类似地,的结果eval必须具有apply类型,因为它被指定为的子类型Lambda,如abstract trait中所指定Lambda。功能范式包括定义许多未按特征分组在一起的参数化类型构造函数。
值级编程和类型级编程之间的比较
abstract class C { val x }trait C { type X }C.x (引用对象C中的字段值/函数x)C#x (在特征C中引用字段类型x)def f(x:X) : Ytype f[x <: X] <: Y称为“类型构造函数”,通常发生在抽象特征中)def f(x:X) : Y = xtype f[x <: X] = x在类型和值之间转换
在许多示例中,通过特征定义的类型通常既是抽象的又是密封的,因此既不能直接实例化也不能通过匿名子类进行实例化。因此,null在使用某种类型的兴趣进行值级计算时,通常将其用作占位符值:
val x:A = null,A您关心的类型在哪里?由于类型擦除,参数化类型看起来都一样。此外,(如上所述),您正在使用的值通常都是null,因此限制对象类型(例如,通过match语句)是无效的。
诀窍是使用隐式函数和值。基本情况通常是隐式值,而递归情况通常是隐式函数。实际上,类型级编程大量使用了隐式。
考虑以下示例(取自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]]后继两次应用于表示零的类型)。
我们可以为各种自然数取别名,以方便参考。例:
type _3 = Succ[Succ[Succ[_0]]]
(这很像将a定义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 }
正如我们将在下面看到,会有从类构造的对象TypeToValue的每个Nat从_0高达(例如)_3,并且每个将存储相应类型(即的值表示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),编译器会寻找ttv类型的隐式参数TypeToValue[_0, Int](因为z类型为_0)。它找到对象_0ToInt,并调用getValue该对象的方法并返回0。需要注意的重要一点是,我们没有向程序指定要使用哪个对象,而是由编译器隐式找到的。
现在让我们考虑一下toInt(y)。这次,编译器将查找ttv类型为隐式的参数TypeToValue[Succ[_0], Int](因为y类型为Succ[_0])。它找到函数succToInt,该函数可以返回适当类型(TypeToValue[Succ[_0], Int])的对象并对其求值。此函数本身采用类型的隐式参数(v)TypeToValue[_0, Int](即,TypeToValue第一个类型参数少的Succ[_])。编译器提供_0ToInt(如toInt(z)上面的评估所述),并使用value succToInt构造一个新TypeToValue对象1。同样,必须注意,编译器将隐式提供所有这些值,因为我们无法显式访问它们。
检查工作
有几种方法可以验证您的类型级计算是否达到了您的期望。这里有一些方法。使两个类型A和B要验证是否相等。然后检查以下代码:
Equal[A, B]
Equal[T1 >: T2 <: T2, T2](取自apocolisp)implicitly[A =:= B]或者,您可以将类型转换为值(如上所示),然后对值进行运行时检查。例如assert(toInt(a) == toInt(b)),where a是type A和btype是B。
其他资源
完整的可用构造集可以在scala参考手册(pdf)的“类型”部分中找到。
Adriaan Moors有一些有关类型构造函数和相关主题的学术论文,并提供了来自scala的示例:
Apocalisp是一个博客,其中包含Scala中许多类型级编程的示例。
ScalaZ是一个非常活跃的项目,它提供使用各种类型级别的编程功能来扩展Scala API的功能。这是一个非常有趣的项目,拥有很多追随者。
MetaScala是Scala的类型级库,包括自然数,布尔值,单位,HList等的元类型。它是Jesper Nordenberg的项目(他的博客)。
该Michid(博客)在斯卡拉型高级编程的一些例子真棒(从对方的回答):
Debasish Ghosh(博客)也有一些相关职位:
(我一直在对此主题进行一些研究,这是我所学到的。我仍然是新手,因此请指出此答案中的任何错误之处。)
除了这里的其他链接,我还有关于Scala中类型级别元编程的博客文章:
正如Twitter上建议的那样:Shapeless: Miles Sabin 在Scala中进行的泛型/多型编程探索。
Scalaz具有源代码,Wiki和示例。