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
(不进行参数化)。#
与点运算符非常相似:.
用于值)。在性状App
lambda演算示例的,类型eval
是这样实现的:type eval = S#eval#apply[T]
。本质上,这是调用eval
trait参数的类型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) : Y
type f[x <: X] <: Y
称为“类型构造函数”,通常发生在抽象特征中)def f(x:X) : Y = x
type 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
和b
type是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和示例。