【问题标题】:Scala type programming resourcesScala 类型编程资源
【发布时间】:2011-05-23 20:45:14
【问题描述】:

根据this question,Scala 的类型系统是Turing complete。有哪些资源可以让新手利用类型级编程的强大功能?

以下是我目前找到的资源:

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

有什么好的介绍资源吗?

【问题讨论】:

  • 就个人而言,我认为想在 Scala 中进行类型级编程的人已经知道如何在 Scala 中进行编程的假设是非常合理的。即使这意味着我对你链接到的那些文章一个字都不懂:-)

标签: scala types


【解决方案1】:

概述

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

范式

类型级编程主要有两种范式:“面向对象”和“函数式”。从这里链接到的大多数示例都遵循面向对象的范例。

可以在 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 Lambdasubstapplyeval
  • 下一步:定义扩展抽象特征并实现各种抽象类型字段的子特征
    • 通常,这些子特征将使用参数进行参数化。在 lambda 演算示例中,子类型是 trait App extends Lambda,它使用两种类型参数化(ST,两者都必须是 Lambda 的子类型),trait Lam extends Lambda 使用一种类型参数化(T),和trait X extends Lambda(未参数化)。
    • 类型字段通常通过引用子特征的类型参数来实现,有时通过哈希运算符引用它们的类型字段:#(与点运算符非常相似:. 用于值)。在 lambda 演算示例的特征 App 中,eval 类型实现如下:type eval = S#eval#apply[T]。这实际上是调用特征参数Seval 类型,并在结果上调用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 &lt;: X] &lt;: Y(这称为“类型构造函数”,通常出现在抽象特征中)
  • 功能实现
    • 价值级别:def f(x:X) : Y = x
    • 类型级别:type f[x &lt;: X] = x
  • 条件
  • 检查相等性
    • 价值级别:a:A == b:B
    • 类型级别:implicitly[A =:= B]
    • value-level:在 JVM 中通过运行时的单元测试发生(即没有运行时错误):
      • 本质上是一个断言:assert(a == b)
    • 类型级别:通过类型检查在编译器中发生(即没有编译器错误):
      • 本质上是一种类型比较:例如implicitly[A =:= B]
      • A &lt;:&lt; B,仅当 AB 的子类型时编译
      • A =:= B,仅当 AB 的子类型且 BA 的子类型时编译
      • A &lt;%&lt; B, ("viewable as") 仅当 A 可被视为 B 时才编译(即存在从 AB 子类型的隐式转换)
      • an example
      • more comparison operators

类型和值之间的转换

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

    • 例如val x:A = null,其中A是你关心的类型
  • 由于类型擦除,参数化类型看起来都一样。此外,(如上所述)您正在使用的值往往都是null,因此对象类型的条件(例如通过匹配语句)是无效的。

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

考虑这个例子(taken from 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]](后继两次应用于表示 0 的类型)。

我们可以给各种自然数起别名,以便于参考。示例:

type _3 = Succ[Succ[Succ[_0]]]

(这很像将val 定义为函数的结果。)

现在,假设我们要定义一个值级函数def toInt[T &lt;: 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 }

正如我们将在下面看到的,对于从 _0 到(例如)_3 的每个 Nat,将有一个从类 TypeToValue 构造的对象,并且每个对象都将存储相应类型的值表示(即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) 时,编译器会查找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)(即,第一个类型参数为TypeToValueSucc[_] 少一个)。编译器提供_0ToInt(正如上面对toInt(z) 的评估所做的那样),succToInt 构造一个新的TypeToValue 对象,其值为1。同样,重要的是要注意编译器隐式提供所有这些值,因为我们无法显式访问它们。

检查您的工作

有多种方法可以验证您的类型级计算是否符合您的预期。这里有一些方法。制作两个类型AB,您要验证它们是否相等。然后检查以下是否编译:

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

或者,您可以将类型转换为值(如上所示)并对值进行运行时检查。例如。 assert(toInt(a) == toInt(b)),其中a 的类型为Ab 的类型为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) 也有一些相关的帖子:

(我一直在对这个主题进行一些研究,这就是我所学到的。我还是新手,所以请指出这个答案中的任何不准确之处。)

【讨论】:

    【解决方案2】:

    【讨论】:

    • 只是想对有趣的博客表示感谢;我已经关注它一段时间了,尤其是上面提到的最后一篇文章,加深了我对面向对象语言的类型系统应该具有的重要属性的理解。所以,谢谢!
    【解决方案3】:

    正如 Twitter 上的建议:Shapeless: An exploration of generic/polytypic programming in Scala Miles Sabin。

    【讨论】:

      【解决方案4】:

      【讨论】:

        【解决方案5】:

        Scalaz 有源代码、wiki 和示例。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-10-30
          • 1970-01-01
          • 1970-01-01
          • 2010-11-25
          • 2011-03-03
          • 2015-05-28
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多