Essential Scala: Sequencing Computations

这节将会学习两个新的语言特性: generics和functions. 和用这些特性创建的两个抽象: functors和monads.

sealed trait IntList { 
    def length: Int = this match {
        case End => 0
        case Pair(hd, tl) => 1 + tl.length
    }
    def double: IntList = this match {
        case End => End
        case Pair(hd, tl) => Pair(hd * 2, tl.double)
    }
    def product: Int = this match {
        case End => 1
        case Pair(hd, tl) => hd * tl.product
    }
    def sum: Int = this match {
        case End => 0
        case Pair(hd, tl) => hd + tl.sum
    } 
}
final case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

这些代码有两个问题,首先是我们的列表限制只能存储Int,第二是里面有很多重复.

Generics 泛型

泛型类型允许我们基于类型进行抽象.

Pandora’s Box

我们创建一个集合,只存储单个的值.我们并不关心box中存储的值的类型,但是当我们把值取出时能够确认类型.

final case class Box[A](value: A) 

Box(2)
// res0: Box[Int] = Box(2)

res0.value
// res1: Int = 2

Box("hi") // 当我们省略参数类型时,Scala会进行自动推断
// res2: Box[String] = Box(hi)

res2.value
// res3: String = hi

语法 [A] 表示类型参数,我们同样给你给方法添加这样的类型参数:

def generic[A](in: A): A = in 

generic[String]("foo")
// res: String = foo

generic(1) // 同样,当我们省略参数类型时,Scala会进行自动推断
// res: Int = 1

类型参数和方法参数的工作原理类似.当我们调用方法时,我们在方法调用中将值和方法参数名绑定在一起.

当我们使用类型参数调用一个方法或构造一个类时,类型参数绑定到方法体或class的具体类型.因此,当我们调用generic(1)时,类型参数A已经在generic体内绑定到Int.

类型参数语法:

// 我们使用由类型名构造的列表来声明泛型类型,通常使用一个大写字母来代表一个泛型类型

// 泛型类型同样可以用于声明class或trait,并且在整个声明中可见
case class Name[A](...){ ... } 
trait Name[A]{ ... }

// 同样用于方法声明,仅方法内部可见
def name[A](...){ ... }

泛型代数数据类型

sealed trait Calculation
final case class Success(result: Double) extends Calculation 
final case class Failure(reason: String) extends Calculation

我们要将以上代码泛型化,以便返回的结果不仅只有Double和String.

一个 A 类型的 Result要么是一个 A 类型的Success,要么是一个带有String类型原因的Failure:

sealed trait Result[A]
case class Success[A](result: A) extends Result[A]
case class Failure[A](reason: String) extends Result[A]

注意,Success和Failure在继承Result时都提供的泛型类型参数A,Success中有一个A类型的参数result,而Failure中只是向Result进行了泛型类型参数声明.

Invariant Generic Sum Type Pattern 用法:

sealed trait A[T]
final case class B[T]() extends A[T] 
final case class C[T]() extends A[T]

Functions

Functions支持我们对方法进行抽象,将方法转换成值然后在程序中进行传递.

sealed trait IntList { 
    def length: Int = this match {
        case End => 0
        case Pair(hd, tl) => 1 + tl.length
    }
    def double: IntList = this match {
        case End => End
        case Pair(hd, tl) => Pair(hd * 2, tl.double)
    }
    def product: Int = this match {
        case End => 1
        case Pair(hd, tl) => hd * tl.product 
    }
    def sum: Int = this match {
        case End => 0
        case Pair(hd, tl) => hd + tl.sum 
    }
}

所有这些方法都有同样的生成模式,如果能够去掉重复的部分将会很nice.

我们把注意力放到返回值类型为Int的方法上:

def abstraction(end: Int, f: ???): Int = this match {
    case End => end
    case Pair(hd, tl) => f(hd, tl.abstraction(f, end)) 
}

方法与函数类似:传入参数然后计算出一个结果值.但不同的是,函数本身就是一个值,我们可以将函数传给一个方法或另一个函数,或者从方法中返回一个函数.

函数类型

表达式 (A, B) => C 表示A和B是参数类型,C为返回值类型.在上面的例子中,我们希望函数 f 接收两个Int类型的参数然后返回一个Int,就可以写作 (Int, Int) => Int .

函数类型声明语法:

(A, B, ...) => C
  1. A和B分别是函数的参数类型
  2. C是函数的返回值类型

如果该函数只有一个参数时,则可以写成:

A => C

函数字面值

Scala同样提供了特定的函数字面值语法以创建新的函数.

val sayHi = () => "Hi!"
// sayHi: () => String = <function0>
sayHi()
// res: String = Hi!
val add1 = (x: Int) => x + 1
// add1: Int => Int = <function1>
add1(10)
// res: Int = 11
val sum = (x: Int, y:Int) => x + y
// sum: (Int, Int) => Int = <function2>
sum(10, 20)
// res: Int = 30

我们一般会省略类型注释,让Scala自己进行推断,但如果需要的话也可以添加上:

(x: Int) => (x + 1): Int

函数字面值声明语法:

(parameter: type, ...) => expression
  1. 可选的parameter即需要传递给函数的参数名
  2. typy即参数的类型
  3. expression即函数的计算过程

Generic Folds for Generic Data

Fold

首先定义一个泛型列表,上面的例子中只能包含Int类型数据:

sealed trait LinkedList[A]
final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A] 
final case class End[A]() extends LinkedList[A]

Int行列表版本的fold方法为:

def fold[A](end: A, f: (Int, A) => A): A = this match {
    case End => end
    case Pair(hd, tl) => f(hd, tl.fold(end, f)) 
}

实现泛型版本的fold:

sealed trait LinkedList[A] {
    def fold[B](end: B, f: (A, B) => B): B = this match {
        case End() => end
        case Pair(hd, tl) => f(hd, tl.fold(end, f))
    } 
}
final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A] 
final case class End[A]() extends LinkedList[A]

Working With Functions

我们可以使用多种窍门编写函数:

  1. 通过紧凑语法编写函数
  2. 将方法转换为函数
  3. 高阶函数

比较简单的情况下使用占位符语法:

((_: Int) * 2)
// res: Int => Int = <function1>

表达式 ((_: Int) 2) 会被编译器解释为 (a: Int) => a 2, 在编译器能够正确推断类型时使用占位符语法则比较符合语言习惯:

_ + _       // expands to `(a, b) => a + b` 
foo(_)      // expands to `(a) => foo(a)` 
foo(_, b)   // expands to `(a) => foo(a, b)` 
_(foo)      // expands to `(a) => a(foo)`
// and so on...

仅被推荐用作比较小的函数.

同样可以将一个方法装换为一个函数,跟占位符语法比较相似的一点是使用一个下划线跟在方法调用后面:

object Sum {
    def sum(x: Int, y: Int) = x + y
}

// 调用方法二不传入参数
Sum.sum
// <console>:9: error: missing arguments for method sum in object Sum;
// follow this method with `_' if you want to treat it as a partially applied function // Sum.sum
//

(Sum.sum _)
// res: (Int, Int) => Int = <function2>

有些场景Scala能够推断我们需要一个函数,这时甚至可以省略掉下划线,Scala会自动将方法转换为函数:

object MathStuff {
    def add1(num: Int) = num + 1
}
Counter(2).adjust(MathStuff.add1) 
// res: Counter = Counter(3)

方法能够同时拥有多个参数列表,只需要将参数列表使用小括号进行分割即可:

def example(x: Int)(y: Int) = x + y 
// example: (x: Int)(y: Int)Int
example(1)(2) // res: Int = 3

使用这种方式重写fold方法将更具可读性:

def fold[B](end: B)(pair: (A, B) => B): B = this match {
    case End() => end
    case Pair(hd, tl) => pair(hd, tl.fold(end, pair)) 
}

fold(0){ (total, elt) => total + elt }

//之前的调用方式
fold(0, (total, elt) => total + elt)

Modelling Data with Generic Types

比如一个方法要返回两种类型:

def intAndString: ??? = // ...
def booleanAndDouble: ??? = // ...

这时可以使用Pair:

def intAndString: Pair[Int, String] = // ...
def booleanAndDouble: Pair[Boolean, Double] = // ...

Tuples

如果一个方法根据参数返回一个或多个类型:

def intOrString(input: Boolean) = if(input == true) 123 else "abc"
// intOrString: (input: Boolean)Any

如果按照这种方式写,编译器会把返回值结果自动推断为Any,然而,我们可以推荐一种新的数据类型:

def intOrString(input: Boolean): Sum[Int, String] = 
    if(input == true) {
        Left[Int, String](123) } 
    else {
        Right[Int, String]("abc")
    }

Sequencing Computation

Map:

sealed trait LinkedList[A] {
    def map[B](fn: A => B): LinkedList[B] = this match {
        case Pair(hd, tl) => Pair(fn(hd), tl.map(fn)) 
        case End() => End[B]()
    } 
}

FlatMap:

sealed trait Maybe[A] {
    def flatMap[B](fn: A => Maybe[B]): Maybe[B] = this match {
        case Full(v) => fn(v)
        case Empty() => Empty[B]()
    } 
}
final case class Full[A](value: A) extends Maybe[A] 
final case class Empty[A]() extends Maybe[A]

如果一个类型 F[A] 带有一个map方法,叫做functor, 如果一个functor带有一个flatMap方法,叫做monad.

Variance 变型

上面的章节已经讲过Variance annotations,支持我们通过类型参数来控制子类之间的关系.

之前定义过的一个Maybe类:

sealed trait Maybe[A]
final case class Full[A](value: A) extends Maybe[A] 
final case class Empty[A]() extends Maybe[A]

但是我喜欢把Empty位置上没有用的类型参数去掉:

sealed trait Maybe[A]
final case class Full[A](value: A) extends Maybe[A] 
final case object Empty extends Maybe[???]

Object不能有类型参数.但是在继承的时候仍然要标识类型参数,如果单纯的使用Unit或Nothing则不能通过编译.

scala> :paste
sealed trait Maybe[A]
final case class Full[A](value: A) extends Maybe[A]
final case object Empty extends Maybe[Nothing]
^D

defined trait Maybe 
defined class Full 
defined module Empty

scala> val possible: Maybe[Int] = Empty 
<console>:9: error: type mismatch;
 found   : Empty.type
 required: Maybe[Int]
    val possible: Maybe[Int] = Empty

这里出现的问题是 Empty是 Maybe[Nothing]类型,但该类型并不属于 Maybe[Int]的子类型.

Invariance, Covariance, and Contravariance

如果我们有一个类型Foo[A],同时A是B的子类,那么Foo[A]是Foo[B]的子类型吗? 这取决于Foo类型的Covariance.

A type Foo[T] is invariant in terms of T, meaning that the types Foo[A] and Foo[B] are unrelated regardless of the rela onship between A and B. This is the default variance of any generic type in Scala.

A type Foo[+T] is covariant in terms of T, meaning that Foo[A] is a supertype of Foo[B] if A is a supertype of B. Most Scala collec on classes are covariant in terms of their contents. We’ll see these next chapter.

A type Foo[-T] is contravariant in terms of T, meaning that Foo[A] is a subtype of Foo[B] if A is a supertype of B. The only example of contravariance that I am aware of is func on arguments.

详细解释参考 协变 与 逆变.