[译] Scala 类型的类型(五)

上一篇

Scala 类型的类型(四)html

目录

21. 结构类型

结构类型(Strucural Types)常常被描述为「类型安全的鸭子类型(duck typing)」,若是你想得到一个直观的理解,这是一个很好的比较。java

迄今为止,咱们在类型方面考虑的都是这样的问题:「它实现了接口 X 吗?」,有了「结构类型」,咱们就能够深刻一步,开始对一个指定对象的结构(所以得名)进行推理。当咱们在检查一个采用告终构类型的类型匹配问题时,咱们须要把问题改成:「这里存在带有这种签名的方法吗?」。编程

让咱们举一个很常见的例子,来看看它为何如此强大。想象你有不少支持被 closed 的东西,在 Java 里,一般会实现 java.io.Closeable 接口,以便写出一些经常使用的 Closeable 工具类(事实上,Google Guava 就有这样的一个类)。如今再想象有人还实现了一个 MyOwnCloseable 类,但没有实现 java.io.Closeable 。因为静态类型的缘故,你的 Closeables 类库就会出问题,你就不能传 MyOwnCloseable 的实例给它。让咱们使用结构类型来解决这个问题:安全

type JavaCloseable = java.io.Closeable
// reminder, it's body is: { def close(): Unit }

class MyOwnCloseable {
  def close(): Unit = ()
}


// method taking a Structural Type
def closeQuietly(closeable: { def close(): Unit }) =
  try {
    closeable.close()
  } catch {
    case ex: Exception => // ignore...
  }


// accepts a java.io.File (implements Closeable):
closeQuietly(new StringReader("example"))

// accepts a MyOwnCloseable
closeQuietly(new MyOwnCloseable)复制代码

这个结构类型被做为方法的一个参数。基本上能够说,咱们对这个类型惟一的指望就是它应该存在内部(close)这样一个方法。它能够拥有更多的方法,所以这里并非一个彻底匹配,而是这个类型必须定义最小的一组方法,这样才能有效。app

另外须要注意的是,使用结构类型对运行时性能存在很大的负面影响,由于实际上它是经过反射实现的。咱们这里再也不经过字节码来调研了,记住查看 scala (或 java)类生成的字节码是一件很容易的事情,只需使用 :javap in the Scala REPL ,因此你应该本身试一试。ide

在咱们进入下一个话题以前,再来说一种精炼的使用风格。想象你的结构类型至关的丰富,好比是一个表明某种事物的类型,你能够打开它,使用它,而后必须关闭。经过使用「类型别名」(在另外一部分中有详细描述)与「结构类型」,咱们就能够将类型定义与方法分离,作法以下:函数

type OpenerCloser = {
  def open(): Unit
  def close(): Unit
}

def on(it: OpenerCloser)(fun: OpenerCloser => Unit) = {
  it.open()
  fun(it)
  it.close()
}复制代码

经过使用这样一个类型别名,def 的部分变得更加清晰了。我极力推荐这种「对更大的结构类型采用类型别名」的作法,同时也最后提醒你们,确认本身是否真的没有其它办法了,再决定采用结构类型。你须要多考虑它负面的性能影响。工具

22. 路径依赖类型

这个类型(Path Dependent Type)容许咱们对类型内部的类型进行「类型检查」,这看起来彷佛比较奇怪,但下面的例子很是直观:布局

class Outer {
  class Inner
}

val out1 = new Outer
val out1in = new out1.Inner // concrete instance, created from inside of Outer

val out2 = new Outer
val out2in = new out2.Inner // another instance of Inner, with the enclosing instance out2

// the path dependent type. The "path" is "inside out1".
type PathDep1 = out1.Inner


// type checks

val typeChecksOk: PathDep1 = out1in
// OK

val typeCheckFails: PathDep1 = out2in
// <console>:27: error: type mismatch;
// found : out2.Inner
// required: PathDep1
// (which expands to) out1.Inner
// val typeCheckFails: PathDep1 = out2in复制代码

这里你能够理解为「每一个外部类都有本身的内部类」。因此它们是不一样的类型 — 差别取决于咱们使用哪一种路径得到。性能

使用这种类型颇有用,咱们可以强制从一个具体参数的内部去得到类型。一个具体的采用该类型的签名以下:

class Parent {
  class Child
}

class ChildrenContainer(p: Parent) {
  type ChildOfThisParent = p.Child

  def add(c: ChildOfThisParent) = ???
}复制代码

咱们如今使用的路径依赖类型,已经被编码到了类型系统的逻辑中。这个容器应该只包含这个 ParentChild 对象,而不是任何 Parent

咱们将很快在 类型投影 章节中看到如何引入任何一个 ParentChild 对象。

23. 类型投影

类型投影(Type Projections)相似「路径依赖类型」,它们容许你引用一个内部类的类型。在语法上看,你能够组织内部类的路径结构,而后经过 # 符号分离开来。咱们先来看看这些路径依赖类型(. 语法)和类型投影(# 语法)的第一个且主要的差异:

// our example class structure
class Outer {
  class Inner
}

// Type Projection (and alias) refering to Inner
type OuterInnerProjection = Outer#Inner

val out1 = new Outer
val out1in = new out1.Inner复制代码

另外一个准确的直觉是相比「路径依赖」,「类型投影」能够用于「类型层面的编程」,如 (存在类型)Existential Types。

「存在类型」是跟「类型擦除」密切相关的东西。

val thingy: Any = ???

thingy match {
  case l: List[a] =>
     // lower case 'a', matches all types... what type is 'a'?!
}复制代码

由于运行时类型被擦除了,因此咱们不知道 a 的类型。咱们知道 List 是一个类型构造器 * -> * ,因此确定有某个类型,它能够用来构造一个有效的 List[T]。这个「某个类型」,就是 存在类型

Scala 为它提供了一种快捷方式:

List[_]
 // ^ some type, no idea which one!复制代码

假设你在使用一些抽象类型成员,在咱们的例子中将会是一些 Monad 。咱们想要强制咱们的使用者只能使用这个 Monad 中的 Cool 实例,由于好比咱们的 Monad 只有针对这些类型才有意义。咱们能够经过这些存在类型 T 的类型边界来实现:

type Monad[T] forSome { type T >: Cool }复制代码

mikeslinn.blogspot.com/2012/08/sca…

译者注:

建议阅读如下文章,以加深对本部分的理解:

24. Specialized Types

24.1. @specialized

类型专业化(Type specialization)与普通的「类型系统的东西」相比,更多的是一种性能方面的技巧。但若是你想编写出良好性能的集合,它是很是重要的,咱们须要掌握它。举个例子,咱们将实现一个很是有用的集合,称为 Parcel[A],它能够保存一个指定类型的值 — 确实有用!

case class Parcel[A](value: A)复制代码

以上是咱们最基本的实现。有什么问题吗?没错,由于 A 能够是任何东西,因此它就会被表示为一个 Java 对象,就算咱们仅对 Int 值进行装箱。所以上面的类会致使对原始值进行装箱和拆箱,由于容器正在处理对象:

val i: Int = Int.unbox(Parcel.apply(Int.box(1)))复制代码

众所周知,当你不是真正须要的时候,装箱不是一个好主意,由于它经过在 intobject Int 之间进行来回转换,产生了更多运行时的工做。怎样才能消除这个问题呢?一种技巧就是将咱们的 Parcel 对全部的原始类型进行「专业化」(这里拿 LongInt 作例子就够了),以下:

若是你已经阅读过 value 类,那么也许已经注意到 Parcel 能够用它很好地代替实现!确实如此。然而,specialized 在 Scala 2.8.1 中就有了,相对地 value 类是在 2.10.x 才被引进。而且,前者可以专业化一种以上的值(虽然它以指数级增加生成代码),value 类却只能限制为一种。

case class Parcel[A](value: A) {
  def something: A = ???
}

// specialzation "by hand"
case class IntParcel(intValue: Int) {
  override def something: Int = /* works on low-level Int, no wrapping! */ ???
}

case class LongParcel(intValue: Long) {
  override def something: Long = /* works on low-level Long, no wrapping! */ ???
}复制代码

IntParcelLongParcel 的实现将有效地避开装箱,由于它们直接在原始值上进行处理,而且无需进入对象领域。如今咱们只需根据咱们的实例,选择想要的 *Parcel

这看起来很好,可是代码基本上变得更难维护了。它有 N 个实现,每种咱们须要支持的原始值类型各一个(如包括:int, long, byte, char, short, float, double, boolean, void, 再加上 Object)! 这须要维护不少样板。

既然咱们已经熟悉了「类型专业化」,也知晓了手动实现它并非很友好,就来看看 Scala 是如何经过引入 @specialized 注解来帮咱们改善这个问题:

case class Parcel[@specialized A](value: A)复制代码

如上所示咱们将 @specialized 注解应用到了类型参数 A 上,从而指示编译器生成该类的全部专业化变量,它们是:ByteParcel, IntParcel, LongParcel, FloatParcel, DoubleParcel, BooleanParcel, CharParcel, ShortParcel, CharParcel 甚至以及 VoidParcel (这并非实际的名字,但你应该明白了大概的意思)。编译器也同时承担调用正确的版本,因此咱们只须要专心写代码,而没必要关心一个类是否被专业化了,编译器会尽量使用适合的版本(若是有的话):

val pi = Parcel(1)     // will use `int` specialized methods
val pl = Parcel(1L)    // will use `long` specialized methods
val pb = Parcel(false) // will use `boolean` specialized methods
val po = Parcel("pi")  // will use `Object` methods复制代码

「太棒了,让咱们尽情使用它吧」 — 这是大部分人发现「专业化」带来的好处以后的反应,由于它能够在下降内存使用率的同时成倍的加速低级操做。不幸的是,它的代价也很高:当使用多个参数时,生成的代码量很快变得巨大,就像这样子:

class Thing[A, B](@specialized a: A, @specialized b: B)复制代码

在上面的例子中,咱们使用了第二种应用专业化的风格 — 加在参数上,这效果等同于咱们直接对 AB 进行专业化。请注意,上述代码将生成 8 * 8 = 64 种实现,由于它必须处理如「A 是一个 int,B是一个 int」以及「A 是一个 boolean,可是 B 是一个 long」的状况 — 你能够看到这是在哪里。事实上生成的类的数量大约在 2 * 10^(nr_of_type_specializations),对于已经有了 3 个类型参数的状况,它很容易达到了数千个类。

有一些方法能够防止这个「指数级爆炸」,例如经过限制专业化的目标类型。假设 Parcel 大部分状况只处理整数,从不跟浮点数打交道,咱们就能够编译器只专业化 LongInt,如:

case class Parcel[@specialized(Int, Long) A](value: A)复制代码

此次让咱们使用 :javap Parcel 来研究一点字节码:

// Parcel, specialized for Int and Long
public class Parcel extends java.lang.Object implements scala.Product,scala.Serializable{
    public java.lang.Object value(); // generic version, "catch all"
    public int value$mcI$sp();       // int specialized version
    public long value$mcJ$sp();}     // long specialized version

    public boolean specInstance$();  // method to check if we're a specialized class impl.
}复制代码

如你所见,编译器提供了额外的专业化方法,如 value$mcI$sp(),它将返回 intlong 也有相似的方法。值得一提的是这里还有另一个叫作 specInstance$ 的方法,若是使用的实现是一个专业化的类,它会返回 true

可能你比较好奇当前在 Scala 中哪些类被专业化了,它们有(可能不完整):Function0, Function1, Function2, Tuple1, Tuple2, Product1, Product2, AbstractFunction0, AbstractFunction1, AbstractFunction2 。因为当前专业化 2 个参数的成本已经很高,一个趋势是咱们不要再专业化更多的参数了,虽然咱们能够这么干。

为何咱们要避免进行装箱,一个典型的例子就是「内存效率」。想象一个 boolean 值,若是它的存储只消耗 1 位那是极好的,惋惜它不是(包含我了解的全部 JVM),例如在 HotSpot 上一个 boolean 被当作一个 int,因此它要占用 4 个字节的空间。它的兄弟 java.lang.Boolean 相似全部 Java 对象同样,则有 8 字节的对象头,而后再存储 boolean (额外增长 4 字节)。因为 Java 对象布局的排列规则,这个对象占用的空间再分配 16 字节(8 个字节给对象头,4 个字节给值,4 个字节给 填充)。这就是为啥咱们但愿避免装箱的另一个悲伤的缘由。

24.2. Miniboxing

❌ 该章节做者还没有完成

这不是 Scala 的一个特性,可是能够与 scalac 一块儿做为编译器插件。

咱们已经在上一节解释了,专业化很是强大,但同时也是一个「编译器炸弹」,具备指数级代码增加的问题。如今已经有一个被证明的概念能够解决这个问题,Miniboxing 是一个编译器插件,它实现了 @specialized 相同的效果,然而却不会生成数千个类。

TODO, there’s a project from withing EPFL to make specialization more efficient: Scala Miniboxing

25. Type Lambda

❌ 该章节做者还没有完成

在 type lambda 的部分咱们会使用 「路径依赖类型」及 「结构类型」,若是你忽略了这两个章节,你能够先跳回去看看。

在了解 Type Lambdas 以前,让咱们先回顾下关于「函数」和「柯里化」的某些细节:

class EitherMonad[A] extends Monad[({type λ[α] = Either[A, α]})#λ] {
  def point[B](b: B): Either[A, B]
  def bind[B, C](m: Either[A, B])(f: B => Either[A, C]): Either[A, C]
}复制代码
相关文章
相关标签/搜索