Scala类型编程资源


102

根据这个问题,Scala的类型系统是图灵完整的。有哪些可用资源使新手能够利用类型级编程的功能?

这是我到目前为止找到的资源:

这些资源很棒,但是我觉得我缺少基础知识,因此没有扎实的基础。例如,哪里有类型定义的介绍?我可以对类型执行哪些操作?

有什么好的入门资源吗?


我个人认为,想要在Scala中进行类型级编程的人已经很合理地知道如何在Scala中进行编程。即使这意味着我不理解这些条款的话,你链接到:-)
约尔格W¯¯米塔格

Answers:


140

总览

类型级编程与传统的价值级编程有很多相似之处。但是,与值级编程不同,值级编程在运行时进行计算,而在类型级编程中,计算在编译时进行。我将尝试在值级别的编程和类型级别的编程之间取得相似之处。

范式

类型级编程中有两种主要范例:“面向对象”和“功能”。从此处链接的大多数示例都遵循面向对象的范例。

在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该保证了以下类型的存在:substapply,和eval
  • 下一步:定义扩展抽象特征并实现各种抽象类型字段的子特征
    • 通常,这些子特性将使用参数进行参数化。在lambda演算示例中,trait App extends Lambda使用两种类型(ST,都必须是的子类型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) : Y
    • 类型级别:(type f[x <: X] <: Y称为“类型构造函数”,通常发生在抽象特征中)
  • 功能实现
    • 价值水平: def f(x:X) : Y = x
    • 类型级别: type f[x <: X] = x
  • 有条件的
  • 检查平等
    • 价值水平: a:A == b:B
    • 类型级别: implicitly[A =:= B]
    • 值级别:在运行时通过单元测试在JVM中发生(即,没有运行时错误):
      • 本质上是一个断言: assert(a == b)
    • 类型级别:通过类型检查在编译器中发生(即,没有编译器错误):
      • 本质上是类型比较:例如 implicitly[A =:= B]
      • A <:< B,仅当A是的子类型时才编译B
      • A =:= B,仅当A是的子类型BB是的子类型时才编译A
      • A <%< B,(“可见为”)仅在A可见为B(即存在从转换A为的子类型B)时才编译
      • 一个例子
      • 更多比较运算符

在类型和值之间转换

  • 在许多示例中,通过特征定义的类型通常既是抽象的又是密封的,因此既不能直接实例化也不能通过匿名子类进行实例化。因此,null在使用某种类型的兴趣进行值级计算时,通常将其用作占位符值:

    • 例如val x:A = nullA您关心的类型在哪里?
  • 由于类型擦除,参数化类型看起来都一样。此外,(如上所述),您正在使用的值通常都是null,因此限制对象类型(例如,通过match语句)是无效的。

诀窍是使用隐式函数和值。基本情况通常是隐式值,而递归情况通常是隐式函数。实际上,类型级编程大量使用了隐式。

考虑以下示例(取自metascalaapocalisp):

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 = nullnull类型的Succ[Succ[Succ[_0]]])值,我们将要toInt(x)返回3

要实现toInt,我们将使用以下类:

class TypeToValue[T, VT](value : VT) { def getValue() = value }

正如我们将在下面看到,会有从类构造的对象TypeToValue的每个Nat_0高达(例如)_3,并且每个将存储相应类型(即的值表示TypeToValue[_0, Int]将存储的值0TypeToValue[Succ[_0], Int]将存储值1等)。注意,TypeToValue有两种类型的参数化:TVTT对应于我们试图为其分配值的类型(在我们的示例中为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])的对象并对其求值。此函数本身采用类型的隐式参数(vTypeToValue[_0, Int](即,TypeToValue第一个类型参数少的Succ[_])。编译器提供_0ToInt(如toInt(z)上面的评估所述),并使用value succToInt构造一个新TypeToValue对象1。同样,必须注意,编译器将隐式提供所有这些值,因为我们无法显式访问它们。

检查工作

有几种方法可以验证您的类型级计算是否达到了您的期望。这里有一些方法。使两个类型AB要验证是否相等。然后检查以下代码:

  • Equal[A, B]
  • implicitly[A =:= B]

或者,您可以将类型转换为值(如上所示),然后对值进行运行时检查。例如assert(toInt(a) == toInt(b)),where a是type Abtype是B

其他资源

完整的可用构造集可以在scala参考手册(pdf)的“类型”部分中找到。

Adriaan Moors有一些有关类型构造函数和相关主题的学术论文,并提供了来自scala的示例:

Apocalisp是一个博客,其中包含Scala中许多类型级编程的示例。

  • Scala中的类型级编程是一些类型级编程的精彩导览,其中包括布尔值,自然数(如上),二进制数,异构列表等。
  • 更多Scala Typehackery是上面的lambda演算实现。

ScalaZ是一个非常活跃的项目,它提供使用各种类型级别的编程功能来扩展Scala API的功能。这是一个非常有趣的项目,拥有很多追随者。

MetaScala是Scala的类型级库,包括自然数,布尔值,单位,HList等的元类型。它是Jesper Nordenberg的项目(他的博客)

Michid(博客)在斯卡拉型高级编程的一些例子真棒(从对方的回答):

Debasish Ghosh(博客)也有一些相关职位:

(我一直在对此主题进行一些研究,这是我所学到的。我仍然是新手,因此请指出此答案中的任何错误之处。)


12

只是想对这个有趣的博客表示感谢;我已经关注了一段时间,尤其是上面提到的最后一篇文章进一步加深了我对面向对象语言的类型系统应具有的重要属性的理解。那谢谢啦!
扎克·斯诺



By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.