Scala类型的编程资源

dsg*_*dsg 101 types scala

根据这个问题,Scala的类型系统是Turing完整的.有哪些资源可以让新手利用类型级编程的强大功能?

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

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

有没有好的入门资源?

dsg*_*dsg 139

概观

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

范式

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

面向对象范例中类型级编程的一个很好的,相当简单的例子可以在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
}
Run Code Online (Sandbox Code Playgroud)

从示例中可以看出,面向对象的类型级编程范例如下:

  • 首先:使用各种抽象类型字段定义一个抽象特征(参见下面的抽象字段).这是一个模板,用于保证所有实现中都存在某些类型字段,而不强制实现.在演算示例中,这对应于trait Lambda该保证了以下类型的存在:subst,apply,和eval.
  • 下一步:定义扩展抽象特征的子特征并实现各种抽象类型字段
    • 通常,这些子特征将使用参数进行参数化.在lambda演算示例中,子类型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使用参数调用T结果.注意,S保证有一个eval类型,因为参数指定它是一个子类型Lambda.类似地,结果eval必须具有apply类型,因为它被指定为Lambda抽象特征中指定的子类型Lambda.

功能范例包括定义许多未在特征中组合在一起的参数化类型构造函数.

价值级编程与类型级编程之间的比较

  • 抽象类
    • 价值层面: abstract class C { val x }
    • 输入级: trait C { type X }
  • 路径依赖类型
    • C.x (引用对象C中的字段值/函数x)
    • C#x (在特征C中引用字段类型x)
  • 功能签名(无实现)
    • 价值层面: def f(x:X) : Y
    • type-level :( 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中发生(即没有运行时错误):
      • 在essense是一个断言: assert(a == b)
    • type-level:通过类型检查在编译器中发生(即没有编译器错误):
      • 本质上是一种类型比较:例如 implicitly[A =:= B]
      • A <:< B,仅在A是的子类型时编译B
      • A =:= B,仅在A是子类型BB是子类型时编译A
      • A <%< B,("可查看为")只有在A可见的情况下编译B(即从A子类型隐式转换B)
      • 一个例子
      • 更多比较运算符

在类型和值之间转换

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

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

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

考虑这个例子(取自metascalaapocalisp):

sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat
Run Code Online (Sandbox Code Playgroud)

在这里,你有一个自然数的peano编码.也就是说,每个非负整数都有一个类型:0的特殊类型,即_0; 并且每个大于零的整数都有一个表单类型Succ[A],其中A是表示较小整数的类型.例如,表示2的类型将是:( Succ[Succ[_0]]后继应用两次到表示零的类型).

我们可以为各种自然数添加别名,以便更方便地参考.例:

type _3 = Succ[Succ[Succ[_0]]]
Run Code Online (Sandbox Code Playgroud)

(这很像定义val一个函数的结果.)

现在,假设我们要定义一个值级函数def toInt[T <: Nat](v : T),它接受一个参数值,v符合Nat并返回一个整数,表示以vs类型编码的自然数.例如,如果我们有值val x:_3 = null(null类型Succ[Succ[Succ[_0]]]),我们会想要toInt(x)返回3.

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

class TypeToValue[T, VT](value : VT) { def getValue() = value }
Run Code Online (Sandbox Code Playgroud)

正如我们将在下面看到,会有从类构造的对象TypeToValue的每个Nat_0高达(例如)_3,并且每个将存储相应类型的值表示(即,TypeToValue[_0, Int]将存储的值0,TypeToValue[Succ[_0], Int]将存储值1等).注意,TypeToValue参数化有两种类型:TVT.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())
Run Code Online (Sandbox Code Playgroud)

我们实施toInt如下:

def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()
Run Code Online (Sandbox Code Playgroud)

为了理解它是如何toInt工作的,让我们考虑它对几个输入的作用:

val z:_0 = null
val y:Succ[_0] = null
Run Code Online (Sandbox Code Playgroud)

当我们调用时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)上面的评估中所做的那样),并succToInt构造一个TypeToValue具有值的新对象1.同样,重要的是要注意编译器隐式提供所有这些值,因为我们没有显式访问它们.

检查你的工作

有几种方法可以验证您的类型级计算是否正在按预期执行.以下是一些方法.使两种类型AB,要确认是相等的.然后检查以下编译:

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

或者,您可以将类型转换为值(如上所示)并对值进行运行时检查.例如assert(toInt(a) == toInt(b)),a类型Ab类型的位置B.

其他资源

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

Adriaan Moors有几篇关于类型构造函数和相关主题的学术论文,以及来自scala的示例:

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

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

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

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

Debasish Ghosh(博客)也有一些相关的帖子:

(我一直在研究这个问题,这是我所学到的.我还是新手,所以请指出这个答案中的任何不准确之处.)


GCl*_*unt 6

正如Twitter上所建议的那样:无形:由Miles Sabin 在Scala中探索泛型/多型编程.


Ken*_*ida 5