Scala 类型的类型(四)html
结构类型(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
的部分变得更加清晰了。我极力推荐这种「对更大的结构类型采用类型别名」的作法,同时也最后提醒你们,确认本身是否真的没有其它办法了,再决定采用结构类型。你须要多考虑它负面的性能影响。工具
这个类型(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) = ???
}复制代码
咱们如今使用的路径依赖类型,已经被编码到了类型系统的逻辑中。这个容器应该只包含这个 Parent
的 Child
对象,而不是任何 Parent
。
咱们将很快在 类型投影 章节中看到如何引入任何一个 Parent
的 Child
对象。
类型投影(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…
建议阅读如下文章,以加深对本部分的理解:
类型专业化(Type specialization)与普通的「类型系统的东西」相比,更多的是一种性能方面的技巧。但若是你想编写出良好性能的集合,它是很是重要的,咱们须要掌握它。举个例子,咱们将实现一个很是有用的集合,称为 Parcel[A]
,它能够保存一个指定类型的值 — 确实有用!
case class Parcel[A](value: A)复制代码
以上是咱们最基本的实现。有什么问题吗?没错,由于 A
能够是任何东西,因此它就会被表示为一个 Java 对象,就算咱们仅对 Int
值进行装箱。所以上面的类会致使对原始值进行装箱和拆箱,由于容器正在处理对象:
val i: Int = Int.unbox(Parcel.apply(Int.box(1)))复制代码
众所周知,当你不是真正须要的时候,装箱不是一个好主意,由于它经过在 int
和 object Int
之间进行来回转换,产生了更多运行时的工做。怎样才能消除这个问题呢?一种技巧就是将咱们的 Parcel
对全部的原始类型进行「专业化」(这里拿 Long
和 Int
作例子就够了),以下:
若是你已经阅读过 value 类,那么也许已经注意到
Parcel
能够用它很好地代替实现!确实如此。然而,specialized
在 Scala2.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! */ ???
}复制代码
IntParcel
和 LongParcel
的实现将有效地避开装箱,由于它们直接在原始值上进行处理,而且无需进入对象领域。如今咱们只需根据咱们的实例,选择想要的 *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)复制代码
在上面的例子中,咱们使用了第二种应用专业化的风格 — 加在参数上,这效果等同于咱们直接对 A
和 B
进行专业化。请注意,上述代码将生成 8 * 8 = 64
种实现,由于它必须处理如「A 是一个 int
,B是一个 int
」以及「A 是一个 boolean
,可是 B 是一个 long
」的状况 — 你能够看到这是在哪里。事实上生成的类的数量大约在 2 * 10^(nr_of_type_specializations)
,对于已经有了 3 个类型参数的状况,它很容易达到了数千个类。
有一些方法能够防止这个「指数级爆炸」,例如经过限制专业化的目标类型。假设 Parcel
大部分状况只处理整数,从不跟浮点数打交道,咱们就能够编译器只专业化 Long
和 Int
,如:
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()
,它将返回 int
, long
也有相似的方法。值得一提的是这里还有另一个叫作 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 个字节给 填充)。这就是为啥咱们但愿避免装箱的另一个悲伤的缘由。
❌ 该章节做者还没有完成
这不是 Scala 的一个特性,可是能够与 scalac 一块儿做为编译器插件。
咱们已经在上一节解释了,专业化很是强大,但同时也是一个「编译器炸弹」,具备指数级代码增加的问题。如今已经有一个被证明的概念能够解决这个问题,Miniboxing 是一个编译器插件,它实现了 @specialized
相同的效果,然而却不会生成数千个类。
TODO, there’s a project from withing EPFL to make specialization more efficient: Scala Miniboxing
❌ 该章节做者还没有完成
在 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]
}复制代码