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

上一篇

Scala 类型的类型(三)html

目录

16. 枚举

Scala 中并无像 Java 同样支持枚举语法,但咱们可使用一些技巧(包含在 Enumeration)来写出相似的东西。java

16.1. Enumeration

在 Scala 2.10.x 版本中能够经过使用 Enumeration 来实现「相似枚举」的结构。git

object Main extends App {

① object WeekDay extends Enumeration {               
②    type WeekDay = Valueval Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value    
  }
④  import WeekDay._                                   

  def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun)

⑤  WeekDay.values filter isWorkingDay foreach println 
}复制代码

① 首先咱们声明一个单例来包含咱们的枚举值,它必须继承 Enumerationgithub

② 在这里,咱们为 Enumeration 内部的 Value 类型定义一个 类型别名 ,由于咱们须要取一个匹配单例名字的名字,后面能够始终经过「WeekDay」来引用它。(是的,这几乎是一个 hack )json

③ 在这里,咱们采用了「多重赋值」,所以每一个 val 左边的变量都被赋值了一个不一样的 Value 类型的实例数组

④ 这个 import 带来了两点:不只支持了在没有 WeekDay 前缀的状况下直接使用 Mon ,同时也在这个做用域中引入了 type WeekDay ,因而咱们能够在下方的方法定义中使用它安全

⑤ 最后,咱们得到了一些 Enumeration 的方法,这些并非魔术,当咱们建立新的 Value 实例时,大部分动做都会发生工具

正如你所见,Scala 中的枚举机制并非内置的,而是经过巧妙地借助类型系统来实现。对于一些使用场景,这也许已经足够了。但当遇到须要增长枚举值以及往每一个值增长行为的时候,它就不能像 Java 那样强大了。性能

16.2. @enum

@enum 注解如今已经不只仅只是一个提议了, 已经处在 Scala 内部的讨论进程中了:Enumeration must DIE...学习

@enum 注解可能会跟「注解宏」一块儿,在未来被支持。在 Scala 改进计划文档中有关于此的描述:enum-sip

@enum
class Day {
  Monday    { def goodDay = false }
  Tuesday   { def goodDay = false }
  Wednesday { def goodDay = false }
  Thursday  { def goodDay = false }
  Friday    { def goodDay = true  }
  def goodDay: Boolean
}复制代码

译者注:做者以上说起的方案已被官方弃用,但 enum 关键字将在 Dotty 中被支持,参见 dotty.epfl.ch/docs/refere…

17. value 类

value 类型(Value Class)在 Scala 内部存在了很长时间,而且你也已经使用过它们不少次了。由于 Scala 中全部的 Number 都使用这个编译器技巧来避免数字值的装箱和拆箱的过程,好比从 intscala.Int 等。提醒下你回想一下 Array[Int] ,它其实在 JVM 中是 int[] ,(若是你对 bytecode 熟悉,会知道它是 JVM 的一种运行时类型:[I])它 会有蛮多性能方面的影响。总的来讲,数字的数组性能很好,但引用的数组就没那么快了。

好的,咱们如今知道了编译器能够在没必要要的时候经过奇技淫巧来避免将 ints 装箱成 Ints 。所以让咱们来看看 Scala 在 2.10.x 以后是如何将这个特性展现给咱们的。这个特性被称为「value 类」,能够至关简单地应用到你现有的类当中。使用它们简单到只要把 extends AnyVal 加到你的类中,同时遵循如下将说起的新规则。若是你不熟悉 AnyVal ,这多是一个很好的学习机会 — 你能够查看 通用类型系统 — Any, AnyRef, AnyVal

让咱们实现一个 Meter 来做为咱们的例子,它将实现一个原生 int 的包装 ,并支持将以「meter」为单位的数字转化为以 Foot 类型的数字。咱们须要上一课,由于没人理解皇室的制度 ;-) 。不过,若是 95% 的时候都使用原生的 meter 值,为何咱们要由于让一个对象包含一个 int 而支付额外的运行时开销?(每一个实例都有好几个字节!)是由于这是一个面向欧洲市场的项目?咱们须要「value 类」的救援!

case class Meter(value: Double) extends AnyVal {
  def toFeet: Foot = Foot(value * 0.3048)
}

case class Foot(value: Double) extends AnyVal {
  def toMeter: Meter = Meter(value / 0.3048)
}复制代码

咱们将在全部的例子中使用样例类(value 类),但它在技术上不是硬性要求的(尽管很是方便)。虽然你也能够经过在一个普通类使用 val 来实现一个 value 类,相比样例类一般会是最佳方案。你可能会问「为何只有一个参数」,这是由于咱们会尽可能避免去包装值,这对于单个值是有意义的,不然咱们就必须在某些地方保持一个元组,这样很快就会变得含糊,同时咱们也将失去「不包装」策略下的性能。所以记住,value 类仅适用于一个值,虽然没人能够说这个参数必须是一个原始类型,它也能够是一个普通类,如 FruitPerson ,咱们有时候依旧能够避免在 value 类中进行包装。

全部你在定义一个 value 类时须要作的,就是拥有一个包含「继承 AnyVal变量」的类,同时遵循一些它的限制。这个变量不必定就是原始类型,它能够是任何东西。这些限制换句话说,就是一个更长的列表,好比一个 value 类型不能包含除了 def 成员外的其它字段,而且不能被扩展,等等。完整的限制清单以及更深刻的例子,能够参加 Scala 文档 — [Value Classes - summary of limitations])(docs.scala-lang.org/overviews/c…) 。

好了,如今咱们拥有了 MeterFoot 值样例类,咱们首先检查下当添加了 extends AnyVal 部分以后,生成的字节码如何使 Meter 从一个普通的样例类,变成一个 value 类:

// case class
scala> :javap Meter

public class Meter extends java.lang.Object implements scala.Product,scala.Serializable{
    public double value();
    public Foot toFeet();
    // ...
}

scala> :javap Meter$
public class Meter$ extends scala.runtime.AbstractFunction1 implements scala.Serializable{
    // ... (skipping not interesting in this use-case methods)
}复制代码

为 value 类生成的字节码以下:

// case value class

scala> :javap Meter
public final class Meter extends java.lang.Object implements scala.Product,scala.Serializable{
    public double value();
    public Foot toFeet();
    // ...
}

scala> :javap Meter$
public class Meter$ extends scala.runtime.AbstractFunction1 implements scala.Serializable{
    public final Foot toFeet$extension(double);
    // ...
}复制代码

有一件事情应该引发咱们的重视,就是当 Meter 做为一个 value 类被建立时,它的伴生对象得到了一个新的方法 — toFeet$extension(double): Foot 。在这个方法成为 Meter 类的实例方法以前,它没有任何参数(因此它是:toFeet(): Foot)。生成的方法被标记为「extension」(toFeet$extension),实际上这也是咱们给这些方法所取得名字。( .NET 开发者已经看到这种趋势了)

因为咱们的 value 类的目标是避免必须分配整个 value 类对象,从而直接跟包装后的值打交道,因此咱们必须中止使用实例方法,由于它们将迫使咱们产生一个包装( Meter )类的实例。咱们能作的事情是,将这个实例方法变成一个「扩展方法」,它将存储在 Meter 的伴生对象中。咱们经过传入 Double 类型值,而不是使用实例的 value: Double 来调用这个扩展方法。

扩展方法的做用跟隐式转换相似(后者是一个更通用,以及更强大的武器),但它是更加简单的一种方式 — 避免了必须分配整个包装后的对象。相对的,隐式转换会须要它来提供「额外的方法」。扩展方法有点采用「重写生成的方法」的路线,以便它们将「要扩展的类型」做为它们第一个参数。举个例子,假如你写了 3.toHexString ,这个方法会经过一个隐式转换被添加到 Int ,然而因为目标是 class RichInt extends AnyVal ,因此一个 value 类的调用并不会致使 RichInt 的分配,而是会被重写成 RichInt$.$MODULE$.toHexString$extension(3),这样子就避免了 RichInt 的分配。

让咱们用新学习到的知识来调查下在 Meter 的例子中,编译器到底为咱们作了什么。源码旁边注释的部分解释了编译器实际上生成的东西。(如此来发现代码运行时发生了什么):

// source code // what the emited bytecode actualy doesval m: Meter  = Meter(12.0)    // store 12.0 val d: Double = m.value * 2    // double multiply (12.0 * 2.0), store val f: Foot   = m.toFeet       // call Meter$.$MODULE$.toFeet$extension(12.0)复制代码

① 有人可能会期待在这里分配一个 Meter 对象,然而因为咱们正在使用一个 value 类,只有被包装的值被存储 — 即咱们在运行时一直在处理的一个原生 double 值。(赋值和类型检查依旧会验证这是否个 Meter 实例)

② 在这里,咱们访问了 value 类的 value(这个字段名的名字没有关系)。请注意,运行时这里操做的是原生的 doubles ,所以没必要像往常一个普通的样例类同样,调用一个 value 的方法。

③ 这里,咱们彷佛在调用一个定义在 Meter 里的实例方法,然而事实上,编译器已经用一个扩展方法调用代替了这个调用,它在 12.0 这个值中传递。咱们得到了一个 Foot 实例… 等一下!可是 Foot 这里也被定义成了一个 value 类,因此在运行时咱们再次获得了一个原生 double

这些都是「扩展方法」和 「value 类」的基础知识。若是你想阅读更多,了解不一样的边界状况,请参考官方关于 value 类的章节,Mark Harrah 在这里用了不少例子,解释得很好。因此除了基本介绍外,我就再也不重复劳动了。

18. 类型类

❌ 该章节做者还没有完成,或须要修改

类型类(Type Class)属于 Scala 中可利用的最强大的模式,能够总结为(若是你比较喜欢华丽的措施)「特定多态」。等到本章结束以后,你就能够理解它了。

Scala 为咱们解决的典型的问题就是,在无需显式绑定两个类的前提下,提供可拓展的 API 。举一个严格绑定的例子,咱们不使用类型类,如扩展一个 Writable 接口,为了让咱们自定义的数据类型可写:

// no type classes yet
trait Writable[Out] {
  def write: Out
}

case class Num(a: Int, b: Int) extends Writable[Json] {
  def write = Json.toJson(this)
}复制代码

使用这种风格,只是扩展和实现了一个接口,咱们将 Num 转化为 Writable ,同时咱们也必须提供 write 的实现,「必须在这里立刻实现」,这使得其余人难以提供不一样的实现 — 它们必须继承实现一个 Num 子类。这里的另外一个痛点是,咱们不能从一个相同的特质继承两次,提供不一样的序列化目标(你不能同时继承 Writable[Json]Writable[Protobuf])。

全部这些问题均可以经过基于类型类的方法解决,而不是直接继承 Writable[Out] 。让咱们试一试,并详细解释下这究竟是如何作的:

trait Writes[In, Out] {                                               
    def write(in: In): Out
  }

② trait Writable[Self] {                                               
    def write[Out]()(implicit writes: Writes[Self, Out]): Out =
      writes write this
  }

③ implicit val jsonNum = Writes[Num, Json] {                            
    def (n1: Num, n2: Num) = n1.a < n1.
  }

case class Num(a: Int) extends Writable[Num]复制代码

① 首先咱们定义下类型类,它的 API 跟以前的 Writable 特质相似,但咱们保持分离,而不是将它们混合到一个写入的类中。这是为了知道咱们用「自类型注解」定义了什么

② 接下来咱们将咱们的 Writable 特质改成使用 Self 进行参数化,并将「目标序列化类型」移动到 write 的签名中。它如今还须要一个隐式的 Writes[Self, Out] 实现,它将处理序列化 — 这就是咱们的类型类

③ 这是类型类的实现。请注意,咱们将实例标记为 implicit ,因此

Universal traits 是 extend Any 的特质,它们应该只有 def ,而且没有初始化代码。

这里做者还须要有不少补充

19. 自身类型注解

「自身类型」(Self Types)可被用来给一个特质混入外部类型,若是一个其余的类使用了这个特质,它也必须提供该特质混入部分的实现。

来看一个例子,该例子中 Service 特质混入了 Module 特质,后者内部提供了其它的 services。咱们能够经过以下的「自身类型注解」来表示:

trait Module {
  lazy val serviceInModule = new ServiceInModule
}

trait Service {
  this: Module =>

  def doTheThings() = serviceInModule.doTheThings()
}复制代码

Service 定义部分的第二行能够被阅读为「 I’m a Module 」。这看起来与继承一个 Module 并没什么两样,究竟哪里不一样呢?

前者意味着咱们必须在实例化一个 Service 的同时也提供 Module

trait TestingModule extends Module { /*...*/ }

new Service with TestingModule复制代码

若是你没有混入所需的特质,就会像以下同样失败:

new Service {}

// class Service cannot be instantiated because it does not conform to its self-type Service with Module
// new Service {}
// ^复制代码

同时你也应该了解,咱们能够利用「自身类型」语法混入多个特质。写到这,让咱们讨论下为何它被叫作 self-type(除了「是的,它看起来很通」的因素)。答案可能要归于这仍是一种流行的使用风格,就像下面这样:

class Service {
  self: MongoModule with APIModule =>

  def delegated = self.doTheThings()
}复制代码

事实上,你可使用任何标识符(不只仅是 this 或者 self),而后在你的类中引用它。

20. 幽灵类型

幽灵类型(Phantom Types)尽管是个古怪的名字,但彷佛很是贴切。它能够被解释为「不是实例的类型」,咱们不直接使用它们,可是能够用来执行一些更严格的逻辑。

咱们要举的例子是一个 Service 类,它有 startstop 方法。如今咱们想要确保你不能「开始」一个已经在运行的服务(类型系统不容许这么干),反之亦然。

一开始咱们先来定义一些标记状态的特质,它们不包含任何逻辑,咱们只会用它们来表示一个服务的状态类型:

sealed trait ServiceState
final class Started extends ServiceState
final class Stopped extends ServiceState复制代码

注意这里给 ServiceState 特质采用了 sealed 来确保不会有人在系统里忽然增长其它状态。同时咱们也把子类型定义为 final ,所以它们也不会被继承,系统不会被引入其它更多状态。

关于 sealed 关键词

sealed 确保全部继承一个类或者特质的行为都必须在相同的一个编译单元。举例而言,若是你在一个 State.scala 的文件里定义了 sealed trait State 以及一些状态实现,这没毛病。然而,若是你不能再其它的文件来继承 State (如 MyStates.scala)。

注意了,以上状况只针对使用了 sealed 关键词的类型有效,但不适用于它的子类。若是你不能在其它文件里继承 State ,可是若是你准备了一个类型如 trait UserDefinedState extends State ,咱们则能够定义更多 UserDefinedState 的子类,即便是经过其它的文件。假如你要阻止这样的状况发生,你应该给你的子类们加上 final,正如咱们在以上例子中所作的。

了解了这些,咱们终于能够来研究如何将它们做为幽灵类型来使用。首先咱们先来定义一个 Service 类,它有一个 State 类型参数。这里请注意了,咱们将不会在这个类中使用任何 State 类型的值!它只是静静地在这里,像一个幽灵 —— 这也是它名字的由来。

class Service[State <: ServiceState] private () {
  def start[T >: State <: Stopped]() = this.asInstanceOf[Service[Started]]
  def stop[T >: State <: Started]() = this.asInstanceOf[Service[Stopped]]
}
object Service {
  def create() = new Service[Stopped]
}复制代码

所以在这个伴身对象里,咱们先建立了一个 Service 的实例,在最开始它的状态是 Stopped 。这个状态也符合类型参数(<: ServiceState)的类型边界,这很好。

当咱们想要开始/中止一个已经存在的 Service 的时候,有趣的事情来了。好比在 start 方法里定义的这个类型边界,只针对一个 T 的值有效,也就是 Stopped 。在咱们的例子中,进行状态切换是一个空操做,它仍是会返回相同的实例,同时显式地转化为所须要的状态。因为这个类型没有被任何东西调用,你也不会在这个操做中遇到类转换异常。

如今咱们使用 REPL 来调查下以上的代码,做为本章节一个很好的收场:

① scala> val initiallyStopped = Service.create() 
  initiallyStopped: Service[Stopped] = Service@337688d3

②  scala> val started = initiallyStopped.start()  
  started: Service[Started] = Service@337688d3

③  scala> val stopped = started.stop()            
  stopped: Service[Stopped] = Service@337688d3

④  scala> stopped.stop()                          
  <console>:16: error: inferred type arguments [Stopped] do not conform to method stop's
                     type parameter bounds [T >: Stopped <: Started]
              stopped.stop()
                      ^

⑤  scala> started.start()                         
  <console>:15: error: inferred type arguments [Started] do not conform to method start's
                     type parameter bounds [T >: Started <: Stopped]
              started.start()复制代码

① 这里咱们建立了一个初始化实例,它开始的状态是 Stopped
② 成功开启一个 Stopped 的 service,返回的类型为 Service[Started]
③ 成功结束一个 Started 的 service,返回的类型为 Service[Stopped]
④ 然而结束一个已经中止的 service (Service[Stopped])是无效的,不能经过编译,注意打印出来的类型边界
⑤ 相似地,结束一个已经开始的 service (Service[Started])是无效的,不能经过编译,注意打印出来的类型边界

正如你所看到的,幽灵类型是另外一种强大的工具,可让咱们的代码更加的类型安全(或者我应该说「状态安全」!?)。

若是你好奇哪些「不是过于疯狂的类库」使用了这些特性,这里一个值得推荐的例子是 Foursquare Rogue(the MongoDB query DSL)。它利用幽灵类型来确保一个 query builder 的状态正确性,如 limit(3) 被正确地调用。

相关文章
相关标签/搜索