Essential Scala: Modelling Data with Traits

Traits

Traits 是用于创建class的模板,同样的原理,class是创建对象的模板. trait和java中interfaces类似,可以把trait看做是接口和抽象类.

Trait声明语法:

trait TraitName { 
    declarationOrExpression 
    ...
}

class Name(...) extends TraitName { 
    ...
}

case class Name(...) extends TraitName { 
    ...
}

Trait和Class的不同

和class一样,trait同样是一组字段和方法的定义,但是有几点重要的不同:

  1. trait不能有构造器,不能直接从一个trait创建对象,可以从一个trait创建一个class,然后从一个class创建对象.
  2. trait可以定义抽象方法:只有名字和类型签名但是没有具体实现.

This or That and Nothing Else: Sealed Traits

在许多用例中,我们可以枚举能够继承一个trait的所有class. 比如构建网站用户时可以设定匿名用户和已登录用户,这两种用户基本涵盖了用户类别的所有可能,因此我们可以创建一个sealed trait,来让编译器帮助我们进行类型检查:

sealed trait Visitor {
    def id: String
    def createdAt: Date
    def age: Long = new Date().getTime() - createdAt.getTime()
}

在使用sealed trait时必须将其子类型也定义在同一个文件内,一旦一个trait被声明为sealed,模式匹配中必须涵盖该trait的所有子类型,否则会发出警告.

sealed trait Visitor { /* ... */ }
final case class User(/* ... */) extends Visitor 
final case class Anonymous(/* ... */) extends Visitor

闭合特质的声明语法:

sealed trait TraitName { 
    ...
}

// 所有继承自该trait的子类型必须定义在同一个文件内
final case class Name(...) extends TraitName { 
    ...
}

Modelling Data with Traits

The Product Type Pattern

第一种模式是为一个包含其他类型数据的数据格式进行建模.

产品类型模式的一般语法:

// If A has a b(with type B) and a c(with type C) write
case class A(b: B, c: C)

trait A { 
  def b: B 
  def c: C
}

The Sum Type Pattern

这种模式是,一个数据结构属于多个不同的类型.比如”A是一个B,或是一个C”.

sealed trait A
final case class B() extends A 
final case class C() extends A

Working With Data

Structural Recursion using Polymorphism

使用多态结构化递归.

如果在trait中声明一个抽象方法,然后在继承自它的两个子类中分别以不同的方式实现该方法,当我们调用该方法是实际上调用的是该实例对应的具体实现.

sealed trait A { 
    def foo: String
}
final case class B() extends A {
    def foo: String = "It's B!"
}
final case class C() extends A {
    def foo: String = "It's C!"
}

当声明一个类型为A的值时,我们可以到具体的实现却是用的B或C:

scala> val anA: A = B() 
anA: A = B()
scala> anA.foo
res1: String = It's B!
scala> val anA: A = C() 
anA: A = C()
scala> anA.foo
res2: String = It's C!

同样可以在trait中定义一个实现,然后在子类中使用override关键字重写该实现:

sealed trait A { 
    def foo: String = "It's A!"
}
final case class B() extends A {
    override def foo: String = "It's B!"
}
final case class C() extends A {
    override def foo: String = "It's C!"
}

行为会和之前保持一致,仍然会选择具体类的实现:

scala> val anA: A = B() 
anA: A = B()
scala> anA.foo
res3: String = It's B!

记住如果在trait中提供了一个默认的实现,那就要保证该实现对所有的子类型都适用.

Structural Recursion using Pattern Matching

使用模式匹配格式化递归.

The Product Type Pa ern Matching Pattern:

// If A has a b(with type B) and a c(with type C), 
// and we want to write a method f that accepts an A and returns an F, write
def f(a: A): F = a match {
    case A(b, c) => ??? 
}

The Sum Type Pa ern Matching Pattern:

// If A is a B or C, and we want to write a method f accepting an A and returning an F, 
// define a pattern matching case for B and C.
def f(a: A): F = a match {
    case B() => ???
    case C() => ??? 
}

A Complete Example

数据描述,一个Feline是一个Lion,Tiger,Panther或者Cat.我们将会简化数据的描述,一个Cat只有一个String名为favoriteFood:

sealed trait Feline
final case class Lion() extends Feline
final case class Tiger() extends Feline
final case class Panther() extends Feline
final case class Cat(favouriteFood: String) extends Feline

现在我们同时使用泛型和模式匹配来实现一个方法. dinner方法将会在问题中返回适合feline的食物,Cat的dinner是favoriteFood,Lion的是antelope,Tiger是 tiger food,Panters是licorice.

然后定义食物类型:

sealed trait Food
final case object Antelope extends Food
final case object TigerFood extends Food
final case object Licorice extends Food
final case class CatFood(food: String) extends Food

然后定义dinner方法返回Food,首先使用泛型:

sealed trait Feline { 
    def dinner: Food
}
final case class Lion() extends Feline {
    def dinner: Food = Antelope
}
final case class Tiger() extends Feline {
    def dinner: Food = TigerFood
}
final case class Panther() extends Feline {
    def dinner: Food = Licorice
}
final case class Cat(favouriteFood: String) extends Feline {
    def dinner: Food = CatFood(favouriteFood)
}

现在使用模式匹配,使用模式匹配实现时有两种选择,一种是直接在Feline中用一个单独的方法实现,一种是在另一个object中实现:

sealed trait Feline { 
    def dinner: Food = this match {
        case Lion() => Antelope
        case Tiger() => TigerFood
        case Panther() => Licorice
        case Cat(favouriteFood) => CatFood(favouriteFood)
    } 
}
object Diner {
    def dinner(feline: Feline): Food = feline match {
        case Lion() => Antelope
        case Tiger() => TigerFood
        case Panther() => Licorice
        case Cat(food) => CatFood(food)
    } 
}

Choosing Which Pattern to Use

现在我们有三种方式实现结构化递归:

  1. 泛型
  2. 在trait中模式匹配
  3. 在一个单独的object中模式匹配

如果一个方法依赖于class的参数或内置方法,则在class内部进行实现.

如果一个方法依赖于其他外部数据(比如需要Cook产生dinner),则在class的外部实现.

如果需要多种具体实现,则需要使用模式匹配并在class的外部实现.

Object-Oriented vs Func onal Extensibility

Add new method Add new data
OO Change existing code Existing code unchanged
FP Exising code unchanged Change existing code