这节将会学习两个新的语言特性: 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
- A和B分别是函数的参数类型
- 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
- 可选的parameter即需要传递给函数的参数名
- typy即参数的类型
- 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
我们可以使用多种窍门编写函数:
- 通过紧凑语法编写函数
- 将方法转换为函数
- 高阶函数
比较简单的情况下使用占位符语法:
((_: 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.
详细解释参考 协变 与 逆变.