5.1 Case Class和Sealed Trait | 81 |
5.2 模式匹配 | 83 |
5.3 传名参数 | 88 |
5.4 隐式参数 | 91 |
5.5 Typeclass推断 | 93 |
@ def
getDayMonthYear(
s:
String
)
=
s match
{
case
s"$day-$month-$year" =>
println(
s"found day: $day, month: $month, year: $year")
case
_ =>
println(
"not a date"
)
}
@ getDayMonthYear(
"9-8-1965"
)
found day:
9
,
month:
8
,
year:
1965
@ getDayMonthYear(
"9-8"
)
not a date
</> 5.1.scala
Snippet 5.1: 使用Scala模式匹配解析简单的字符串
本章涉及Scala一些更有趣,且不同寻常的特性。我们不仅介绍特性本身,也用一些例子让你感受特性是如何发挥作用的。
本章的某些特性并不会在你日常使用中频繁出现。然而,当你最终在实际中碰到它们时,这能帮助你从一个较高的角度理解它们。
有点像普通的 case
class
,但它代表这个类只是一份数据:其中所有数据都是不可变的,可公开访问的,不携带任何可变状态或封装。它类似于C/C++的“structs”,Java的“POJOs”,Python或Kotlin的“Data Classes”。它的名字来源于可搭配 class
关键字做 模式匹配 (5.2) 的功能。case
Case class使用
关键字定义,不用 case
也能实例化,其构造器参数默认都是公开访问字段。new
|
|
已经实现了下列方法:case
class
展示构造器参数值.
toString
检查构造器参数值是否相等==
方便地创建实例副本.
copy
|
|
如同普通类,你可以在
内部定义实例方法或属性:case
class
|
|
是大型元组的良好替代物,因为你不用 case
class
.
_1.
_2
抽取字段,而是使用字段名称,比如 .
_7
和 .
x
。这比记住元组的字段 .
y
所代表的含义要容易的多!.
_7
可以用 trait
修饰,只被固定数量的 sealed
继承。下列例子中,我们定义了 case
class
,它被 sealed
trait
PointPoint2D
和 Point3D
继承:
@ {
sealed
trait
Point
case
class
Point2D(
x:
Double
,
y:
Double
)
extends
Point
case
class
Point3D(
x:
Double
,
y:
Double
,
z:
Double
)
extends
Point
}
@ def
hypotenuse(
p:
Point)
=
p match
{
case
Point2D(
x,
y)
=>
math.
sqrt(
x *
x +
y *
y)
case
Point3D(
x,
y,
z)
=>
math.
sqrt(
x *
x +
y *
y +
z *
z)
}
@ val
points:
Array[
Point]
=
Array(
Point2D(
1
,
2
)
,
Point3D(
4
,
5
,
6
)
)
@ for
(
p <-
points)
println(
hypotenuse(
p)
)
2.23606797749979
8.774964387392123
</> 5.8.scala
普通
和 trait
之间的核心区别如下:sealed
trait
普通
是开放的:继承类的数量没有限制,只要它们实现所有要求的方法,在 trait
指定接口调用的地方,那些类的实例可互换。trait
是封闭的:仅允许固定数量类继承它,所有继承类必须和trait自身在同一个文件中定义,或者在REPL命令中一同定义(因此要求上述 sealed
trait
Point
/Point2D
/Point3D
的定义要用花括号
包裹)。{
}
由于
继承类数量是固定的,因此我们可以使用上述函数 sealed
trait
Point
进行模式匹配,定义每一种 def
hypotenusePoint
的处理逻辑。
普通
和 trait
在Scala应用中都很常见:普通 sealed
trait
可以有任意多子类,而 trait
子类数量是固定的。sealed
trait
普通
和 trait
让许多事情变得简单:sealed
trait
普通
层次结构使得添加额外子类是容易的:只需要定义你的子类并实现必要方法。但是这让 trait
添加接口变得困难:新接口需要添加到所有已存在的子类,而子类数量可能很多。trait
层次结构是相反的:为 sealed
trait
添加接口很容易,因为新接口可以简单地模式匹配每一个子类,并确定每个子类的处理逻辑。但是添加新子类变得困难,因为你需要为每个已存在的模式匹配接口添加 trait
分支来处理你的新子类,而接口数量可能很多。case
通常,当你预计子类数量几乎不变时,就选择
。一个能用 sealed
trait
建模的好例子是JSON:sealed
trait
@ {
sealed
trait
Json
case
class
Null(
)
extends
Json
case
class
Bool(
value:
Boolean
)
extends
Json
case
class
Str(
value:
String
)
extends
Json
case
class
Num(
value:
Double
)
extends
Json
case
class
Arr(
value:
Seq[
Json]
)
extends
Json
case
class
Dict(
value:
Map[
String
,
Json]
)
extends
Json
}
</> 5.9.scala
一个JSON值只可能是null,boolean,number,string,array,或者dictionary。
JSON在20年来没有发生变化,所以不太可能有人将来会继承我们的JSON
去添加新子类。trait
尽管子类集合是固定的,但是我们在JSON数据上可以进行的操作是没有边界的:解析、序列化、友好打印、最小化、净化等等。
因此用封闭的
,而不是普通开放的 sealed
trait
去建模JSON才是合理的。trait
Scala使用
关键字做模式匹配。这和其它编程语言中的 match
switch
语句相似但更灵活:除了匹配原始类型的integer和string,你还可以从组合数据类型诸如元组和
中解构内容。注意下列许多例子中,有一个 case
class
从句定义了默认case,如果排在前面的case都没有匹配上,将使用该case进行处理。case
_ =>
5.2.1.1 匹配 Int
| 5.2.1.2 匹配 String
|
5.2.1.3 匹配元组 ( Int , Int )
| 5.2.1.4 匹配元组 ( Boolean , Boolean )
|
5.2.1.5 匹配Case Class:
| 5.2.1.6 解构字符串模式:
(注意字符串模式匹配只支持简单的通配符,不支持正则表达式。你可以使用 |
模式可以嵌套,下面例子展示了
嵌套字符串的模式匹配:case
class
@ case
class
Person(
name:
String
,
title:
String
)
@ def
greet(
p:
Person)
=
p match
{
case
Person(
s"$firstName $lastName",
title)
=>
println(
s"Hello $title $lastName")
case
Person(
name,
title)
=>
println(
s"Hello $title $name")
}
@ greet(
Person(
"Haoyi Li"
,
"Mr"
)
)
Hello Mr Li
@ greet(
Person(
"Who?"
,
"Dr"
)
)
Hello Dr Who?
</> 5.16.scala
模式可以嵌套任意层。下面例子展示了元组嵌套
,里面又嵌套字符串的模式匹配:case
class
@ def
greet2(
husband:
Person,
wife:
Person)
=
(
husband,
wife)
match
{
case
(
Person(
s"$first1 $last1",
_)
,
Person(
s"$first2 $last2",
_)
)
if
last1 ==
last2 =>
println(
s"Hello Mr and Ms $last1")
case
(
Person(
name1,
_)
,
Person(
name2,
_)
)
=>
println(
s"Hello $name1 and $name2")
}
@ greet2(
Person(
"James Bond"
,
"Mr"
)
,
Person(
"Jane Bond"
,
"Ms"
)
)
Hello Mr and Ms Bond
@ greet2(
Person(
"James Bond"
,
"Mr"
)
,
Person(
"Jane"
,
"Ms"
)
)
Hello James Bond and Jane
</> 5.17.scala
val
变量你还会在两个地方用到模式匹配:
循环内部和 for
变量定义。当你迭代元组集合时,val
循环中的模式匹配很有用:for
@ val
a =
Array[
(
Int
,
String
)
]
(
(
1
,
"one"
)
,
(
2
,
"two"
)
,
(
3
,
"three"
)
)
@ for
(
(
i,
s)
<-
a)
println(
s +
i)
one1
two2
three3
</> 5.18.scala
当你确信变量值会匹配上给定的模式,并且你唯一想做的就是抽取内容,那就用
语句的模式匹配。如果变量值匹配失败,将会抛出异常:val
|
|
模式匹配让你优雅地处理包含case class和sealed trait的结构化数据。让我们看一个用sealed trait建模的算术表达式:
@ {
sealed
trait
Expr
case
class
BinOp(
left:
Expr,
op:
String
,
right:
Expr)
extends
Expr
case
class
Literal(
value:
Int
)
extends
Expr
case
class
Variable(
name:
String
)
extends
Expr
}
</> 5.21.scala
其中 BinOp
代表二目运算,它可以表达算术计算,如下所示
|
|
|
|
|
|
我们暂时忽略解析过程(即把左边的字符串转变成右边的
),我们将在 Chapter 19: Parsing Structured Text 介绍这个过程。算术表达式解析为 case
class
后,让我们来考虑你可能做的两件事情:以人类可读的字符串打印它;或者给定变量值的情况下,求表达式的值。case
class
可以使用下列算法把表达式转换成字符串:
Expr
是一个 Literal
,字符串就是其字面量值Expr
是一个 Variable
,字符串就是该变量的名称Expr
是一个 BinOp
,字符串就是左表达式字符串化的结果,加上运算符,再加上右表达式字符串化的结果转换成模式匹配的代码可以这样写:
@ def
stringify(
expr:
Expr)
:
String
=
expr match
{
case
BinOp(
left,
op,
right)
=>
s"(${
stringify(
left)
}
$op ${
stringify(
right)
}
)"
case
Literal(
value)
=>
value.
toString
case
Variable(
name)
=>
name
}
</> 5.24.scala
我们可以构造一些之前看到的 Expr
,把它们传递给函数 stringify
来看看输出:
|
|
估值比表达式字符串化稍微复杂一些。我们需要传递一个保存所有变量值的映射 values
,然后我们需要区别对待
,+
,以及 -
运算符:*
@ def
evaluate(
expr:
Expr,
values:
Map[
String
,
Int
]
)
:
Int
=
expr match
{
case
BinOp(
left,
"+"
,
right)
=>
evaluate(
left,
values)
+
evaluate(
right,
values)
case
BinOp(
left,
"-"
,
right)
=>
evaluate(
left,
values)
-
evaluate(
right,
values)
case
BinOp(
left,
"*"
,
right)
=>
evaluate(
left,
values)
*
evaluate(
right,
values)
case
Literal(
value)
=>
value
case
Variable(
name)
=>
values(
name)
}
@ evaluate(
smallExpr,
Map(
"x"
->
10
)
)
res56:
Int
=
11
@ evaluate(
largeExpr,
Map(
"x"
->
10
,
"y"
->
20
)
)
res57:
Int
=
209
</> 5.27.scala
总的来说,这看起来比较类似我们之前编写的函数 stringify
:递归地匹配 expr
参数,处理 :
ExprExpr
每一种
子类。处理无子节点的 case
class
Literal
和 Variable
比较简单,处理 BinOp
时,需要递归估值左孩子和右孩子,最后合并结果。这是任何语言处理递归数据结构的通用方式,Scala用
,sealed
trait
,和模式匹配实现得简洁而轻松。case
class
我们简化了 Expr
结构,以及编写的字符串化函数和估值函数,只是为了看看模式匹配辅以
和 case
class
,如何轻松地处理结构化的数据。我们会在 Chapter 20: Implementing a Programming Language 更深入地探索这些技术。sealed
trait
@ def
func(
arg:
=>
String
)
=
.
.
.
Scala用语法
代表传名参数 (by-name),在方法内部引用参数时会对其估值。它主要有三个应用场景::
=>
T
如下的 log
方法使用了传名参数,参数被真正打印时才对其估值。当logging被禁用时不会对其估值,这能节省构造日志消息 (
) 所花费的CPU时间:"Hello "
+
123
+
"World"
|
|
方法一般不会把所有入参都用上,正如上述例子中,只在需要时才构造日志消息,我们能显著节省CPU时间和对象分配,这在性能敏感的应用中大有帮助。
我们在 Chapter 4: Scala集合 见到的 getOrElse
和 getOrElseUpdate
也使用了这个技术:如果我们查询的值已经存在,那么这两个方法就不会使用默认参数。把一个传名参数作为默认参数,在没有用到它时,我们无需对其估值。
传名参数把估值包裹在前置、后置代码中是另一个常见模式。下述函数 measureTime
推迟 f
的估值,在参数估值前后运行 :
=>
Unit
System
来打印耗时:.
currentTimeMillis(
)
@ def
measureTime(
f:
=>
Unit
)
=
{
val
start =
System.
currentTimeMillis(
)
f
val
end =
System.
currentTimeMillis(
)
println(
"Evaluation took "
+
(
end -
start)
+
" milliseconds"
)
}
@ measureTime(
new
Array[
String
]
(
10
*
1000
*
1000
)
.
hashCode(
)
)
Evaluation took 24
milliseconds
@ measureTime {
// methods taking a single arg can also be called with curly brackets
new
Array[
String
]
(
100
*
1000
*
1000
)
.
hashCode(
)
}
Evaluation took 287
milliseconds
</> 5.30.scala
这种包裹方法还有许多用途:
try
-catch
代码块中估值参数,以便处理异常Future
中估值参数,代码逻辑会在另一个线程中异步运行传名参数在这些案例中都能发挥作用。
传名参数的另一个用例是对方法入参重复估值。下面的代码片段定义了通用的 retry
方法:这个方法接收一个输入参数,在
-try
代码块中估值,如果执行失败了且还在最大尝试次数内,就重新执行它。我们用它包裹一个可能失败的函数调用,检查打印到控制台的重试日志。catch
|
|
上面我们定义了一个类型参数
的泛型函数 [
T]
retry
,接受一个估值结果类型为 T
的传名参数,当代码块执行成功后返回一个类型为 T
的值。我们可以用 retry
包裹任何类型的代码块,在最大可尝试次数内它会不断重试直到成功,然后返回结果。
让 retry
接收一个传名参数,可以在必要时对 requests
代码块重复估值。其它重复估值的用例还包括性能基准测试或者压力测试。你不会经常使用传名参数,但必要时,它能够让你便利地操控参数的估值:比如调校、重复、或跳过。.
get
我们将在 Chapter 12: Working with HTTP APIs 中学习更多上面用到的 requests
库。
隐式参数 是函数调用时自动为你填充的参数。举个例,有一个类 Foo
和一个接收参数
的函数 implicit
foo:
Foobar
:
@ class
Foo(
val
value:
Int
)
@ def
bar(
implicit
foo:
Foo)
=
foo.
value +
10
</> 5.33.scala
如果你调用 bar
时的作用域中找不到隐式的 Foo
,你将得到一个编译错误。调用 bar
时,你需要定义一个类型为 Foo
的隐式值,这样一来 bar
在调用时,才能自动找到隐式值:
|
|
隐式参数和我们在 Chapter 3: Scala基础 遇到的 默认值 很相似。两者都允许你直接传递一个值,或者兜底到某个默认值。主要区别是默认值“硬编码”在方法定义处,而隐式参数在调用处的作用域里寻找
作为默认值。implicit
我们先看一个更具体的例子,例子中隐式参数让你的代码干净可读。之后我们再学习这个特性在 Typeclass推断 (5.5) 中更高级的用法。
下面代码例子中,由于 Future
需要在 ExecutionContext
中执行,于是我们到处传递 ExecutionContext
,使得编码过程乏味、繁琐:
def
getEmployee(
ec:
ExecutionContext,
id:
Int
)
:
Future[
Employee]
=
.
.
.
def
getRole(
ec:
ExecutionContext,
employee:
Employee)
:
Future[
Role]
=
.
.
.
val
executionContext:
ExecutionContext =
.
.
.
val
bigEmployee:
Future[
EmployeeWithRole]
=
{
getEmployee(
executionContext,
100
)
.
flatMap(
executionContext,
e =>
getRole(
executionContext,
e)
.
map(
executionContext,
r =>
EmployeeWithRole(
e,
r)
)
)
}
</> 5.36.scala
getEmployee
和 getRole
异步执行,其中使用 map
和 flatMap
做进一步处理。Future
工作原理超出了本节范围,现在只需要注意到每个操作都需要传递 executionContext
。我们会在 Chapter 13: Fork-Join Parallelism with Futures 重新审视这些API。
假如不使用隐式参数,我们有下列选择:
不停地手动传递 executionContext
:这让你的代码很难阅读,我们真正关心的逻辑被淹没在样板式传递 executionContext
的海洋里
把 executionContext
作为全局变量:这会很简洁,但是在程序不同部分传递不同值的灵活性有所损失
把 executionContext
放进thread-local变量:这可以维持灵活性和简洁性,但是易于出错,你容易忘记为需要它才能运行的代码提前设置好thread-local
所有这些选项都是折中办法,让我们牺牲掉简洁性、灵活性、或者安全性。Scala隐式参数提供了第四种选项:间接地传递 executionContext
,同时给我们简洁、灵活、以及安全。
我们可以让所有函数接收隐式参数 executionContext
来解决这个问题。Future
的 flatMap
,map
等标准库操作已经满足要求,我们可以修改 getEmployee
和 getRole
来依样画葫芦。通过定义 executionContext
为
,下面所有方法调用会自动选中该隐式变量。implicit
def
getEmployee(
id:
Int
)
(
implicit
ec:
ExecutionContext)
:
Future[
Employee]
=
.
.
.
def
getRole(
employee:
Employee)
(
implicit
ec:
ExecutionContext)
:
Future[
Role]
=
.
.
.
implicit
val
executionContext:
ExecutionContext =
.
.
.
val
bigEmployee:
Future[
EmployeeWithRole]
=
{
getEmployee(
100
)
.
flatMap(
e =>
getRole(
e)
.
map(
r =>
EmployeeWithRole(
e,
r)
)
)
}
</> 5.37.scala
使用隐式参数可以帮助你清除应用程序中传递共享上下文或者配置对象的冗余代码:
通过间接传递“不感兴趣”的参数,可以把读者的注意力聚焦在应用程序的核心逻辑上。
由于隐式参数可以被直接传递,它们为开发者保留了灵活性,以防他们想要手动指定或者覆盖正被传递的隐式参数。
隐式值缺失会引起编译错误,这使得它比thread-local用起来不那么容易犯错。在部署到生产环境前,缺失隐式值的编译错误就会被捕获。
隐式参数第二个发挥作用的地方是可以为指定类型关联隐式值,常称为 typeclass。这个术语来自Haskell编程语言,尽管它和
或者 type
都没有关系。typeclass构建在 class
语言特性之上,它很有趣并且足够重要,值得在本章单独占据一节内容。implicit
让我们考虑一个解析命令行参数的需求,输入是
,要求转换成Scala各种数据类型:String
,Int
,Boolean
等等。这是一个几乎所有程序都要面对的通用需求,无论是直接处理,还是借助某个库。Double
第一种尝试可能是编写一个泛型方法来解析各种类型的值。签名类似这样:
def
parseFromString[
T]
(
s:
String
)
:
T =
.
.
.
val
args =
Seq(
"123"
,
"true"
,
"7.5"
)
val
myInt =
parseFromString[
Int
]
(
args(
0
)
)
val
myBoolean =
parseFromString[
Boolean
]
(
args(
1
)
)
val
myDouble =
parseFromString[
Double
]
(
args(
2
)
)
</> 5.38.scala
这乍看起来无法实现:
parseFromString
如何知道怎样把一个
转换成任意类型 String
T
?
它如何知道一个命令行参数可以转为哪些类型,不可以转为哪些类型?比如,我们无法支持从字符串解析出 java
。.
net.
DatagramSocket
第二种尝试也许是定义独立的解析器对象,每种待解析的类型对应一个解析器对象。举个例:
trait
StrParser[
T]
{
def
parse(
s:
String
)
:
T }
object
ParseInt extends
StrParser[
Int
]
{
def
parse(
s:
String
)
=
s.
toInt }
object
ParseBoolean extends
StrParser[
Boolean
]
{
def
parse(
s:
String
)
=
s.
toBoolean }
object
ParseDouble extends
StrParser[
Double
]
{
def
parse(
s:
String
)
=
s.
toDouble }
</> 5.39.scala
然后我们可以如下调用:
val
args =
Seq(
"123"
,
"true"
,
"7.5"
)
val
myInt =
ParseInt.
parse(
args(
0
)
)
val
myBoolean =
ParseBoolean.
parse(
args(
1
)
)
val
myDouble =
ParseDouble.
parse(
args(
2
)
)
</> 5.40.scala
这样是可行的。但这引出了另一个问题:如果我们想要编写一个方法并不是直接解析
,而是解析控制台输入,我们怎样做到?我们或许有两个选项。String
第一种选择是编写全新系列
,去专门解析控制台输入:object
trait
ConsoleParser[
T]
{
def
parse(
)
:
T }
object
ConsoleParseInt extends
ConsoleParser[
Int
]
{
def
parse(
)
=
scala.
Console.
in.
readLine(
)
.
toInt
}
object
ConsoleParseBoolean extends
ConsoleParser[
Boolean
]
{
def
parse(
)
=
scala.
Console.
in.
readLine(
)
.
toBoolean
}
object
ConsoleParseDouble extends
ConsoleParser[
Double
]
{
def
parse(
)
=
scala.
Console.
in.
readLine(
)
.
toDouble
}
val
myInt =
ConsoleParseInt.
parse(
)
val
myBoolean =
ConsoleParseBoolean.
parse(
)
val
myDouble =
ConsoleParseDouble.
parse(
)
</> 5.41.scala
第二种选择是定义一个帮助类,接收一个 StrParser
作为参数,传进去后让StrParser来解析出类型 [
T]
T
def
parseFromConsole[
T]
(
parser:
StrParser[
T]
)
=
parser.
parse(
scala.
Console.
in.
readLine(
)
)
val
myInt =
parseFromConsole[
Int
]
(
ParseInt)
val
myBoolean =
parseFromConsole[
Boolean
]
(
ParseBoolean)
val
myDouble =
parseFromConsole[
Double
]
(
ParseDouble)
</> 5.42.scala
这两种解决方案都很笨重:
第一种方案会造成重复编写
/Int
/Boolean
等等解析器。如果我们希望从网络输入解析,从文件解析时,我们将需要为每种案例重复编写解析器。Double
第二种方案需要我们到处传递 ParseFoo
对象。而我们能够传递给 parseFromConsole
一般仅有 [
Int
]
StrParser
。为什么不让编译器帮我们推断呢?[
Int
]
解决办法是把那些 StrParser
单实例变作
:implicit
trait
StrParser[
T]
{
def
parse(
s:
String
)
:
T }
object
StrParser{
implicit
object
ParseInt extends
StrParser[
Int
]
{
def
parse(
s:
String
)
=
s.
toInt
}
implicit
object
ParseBoolean extends
StrParser[
Boolean
]
{
def
parse(
s:
String
)
=
s.
toBoolean
}
implicit
object
ParseDouble extends
StrParser[
Double
]
{
def
parse(
s:
String
)
=
s.
toDouble
}
}
</> 5.43.scala
我们把
,implicit
object
ParseIntParseBoolean
等等放在
中,object
StrParser
和 object
StrParser
同名,紧挨在一起定义。和 trait
StrParser
/class
紧挨定义的同名 trait
称为 伴生对象 (companion object)。伴生对象一般用于把隐式值、静态方法、工厂方法、以及其它功能聚在一起,它们和一个 object
或者 trait
相关,但是不属于任何特定实例。伴生对象中的隐式值会被自动引入代码当前作用域,提供所需隐式参数。class
注意当你输入到Ammonite Scala REPL时,你需要用额外的花括号
包裹两者的定义,这样才能在同一个REPL命令中同时定义 {
.
.
.
}
和 trait
。object
现在,我们仍然可以像之前一样直接调用 ParseInt
解析字符串字面量,也可以编写泛型函数,根据待解析类型自动选择正确的 .
parse(
args(
0
)
)
StrParser
实例:
def
parseFromString[
T]
(
s:
String
)
(
implicit
parser:
StrParser[
T]
)
=
{
parser.
parse(
s)
}
val
args =
Seq(
"123"
,
"true"
,
"7.5"
)
val
myInt =
parseFromString[
Int
]
(
args(
0
)
)
val
myBoolean =
parseFromString[
Boolean
]
(
args(
1
)
)
val
myDouble =
parseFromString[
Double
]
(
args(
2
)
)
</> 5.44.scala
这和我们最初始的尝试类似,区别在于函数接收一个参数
,可以为待解析类型自动推断相应的 (
implicit
parser:
StrParser[
T]
)
StrParser
。
把 StrParser
变为隐式意味着我们可以重用它们,而不必重复编写解析器或者手动传递它们。比如,我们可以这样编写能够解析控制台字符串的函数:[
T]
def
parseFromConsole[
T]
(
implicit
parser:
StrParser[
T]
)
=
{
parser.
parse(
scala.
Console.
in.
readLine(
)
)
}
val
myInt =
parseFromConsole[
Int
]
</> 5.45.scala
调用 parseFromConsole
自动推断出伴生对象 [
Int
]
StrParser
中的隐式单例 StrParser
,你不必复制它,也不必繁琐地传递它。这就使得编写代码处理泛型 .
ParseIntT
很容易,只要 T
有对应的 StrParser
即可。
隐式参数携带泛型的技术很常用,Scala为此提供了专用语法。下面的方法签名:
def
parseFromString[
T]
(
s:
String
)
(
implicit
parser:
StrParser[
T]
)
=
.
.
.
可以简洁地写做:
def
parseFromString[
T:
StrParser]
(
s:
String
)
=
.
.
.
这个语法就是 上下文界定 (context bound),它和上述语法
语义上等价。当使用上下文界定语法时,隐式参数没有名称,我们不能够像之前那样调用 (
implicit
parser:
StrParser[
T]
)
。但我们可以使用函数 (
implicit
parser:
StrParser[
T]
)
implicitly
解析隐式值,比如 implicitly
。[
StrParser[
T]
]
.
parse
由于Typeclass推断使用了同样的
语言特性,当使用了不合法的类型调用 implicit
parseFromConsole
时,会产生编译错误:
@ val
myDatagramSocket =
parseFromConsole[
java.
net.
DatagramSocket]
cmd19.sc:1: could not find implicit value for parameter parser:
ammonite.$sess.cmd11.StrParser[java.net.DatagramSocket]
val myDatagramSocket = parseFromConsole[java.net.DatagramSocket]
^
Compilation Failed
</> 5.46.scala
除此之外,在另一个方法中调用需要隐式参数
的方法时,编译器也会抛出错误:(
implicit
parser:
StrParser[
T]
)
@ def
genericMethodWithoutImplicit[
T]
(
s:
String
)
=
parseFromString[
T]
(
s)
cmd2.sc:1: could not find implicit value for parameter parser:
ammonite.$sess.cmd0.StrParser[T]
def genericMethodWithoutImplicit[T](s: String) = parseFromString[T](s)
^
Compilation Failed
</> 5.47.scala
我们能用Typelcass推断完成的绝大多数事情,也可以用运行时反射实现。然而,依赖运行时反射是脆弱的,非常容易出bug,或者误配置,在发现前便上了生产环境,引发灾难。对比之下,Scala
特性让你安全地完成同样的事情:错误提前在编译期暴露,你可以从容修复它们,而不是顶着生产系统宕机的压力去解决问题。implicit
我们已经看到了如何使用typeclass技术自动基于待解析类型选择 StrParser
。这同样适用于更复杂的类型,比如我们告诉编译器我们想要一个 Seq
,[
Int
]
,或者甚至嵌套类型 (
Int
,
Boolean
)
Seq
,然后编译器自动组装逻辑,解析出我们想要的类型。[
(
Int
,
Boolean
)
]
比如,下列函数 ParseSeq
为任意类型 T
提供了 StrParser
,其自身又使用了一个隐式 [
Seq[
T]
]
StrParser
:[
T]
implicit
def
ParseSeq[
T]
(
implicit
p:
StrParser[
T]
)
=
new
StrParser[
Seq[
T]
]
{
def
parse(
s:
String
)
=
s.
split(
','
)
.
toSeq.
map(
p.
parse)
}
</> 5.48.scala
不像我们先前定义的单实例
,这里我们使用 implicit
object
。取决于类型 implicit
def
T
,我们需要不同的 StrParser
,对应不同的 [
T]
StrParser
。[
Seq[
T]
]
每次被调用时,不同类型 implicit
def
ParseSeqT
会返回不同 StrParser
。
有了这个定义,我们现在可以解析 Seq
,[
Boolean
]
Seq
等等。[
Int
]
@ parseFromString[
Seq[
Boolean
]
]
(
"true,false,true"
)
res99:
Seq[
Boolean
]
=
ArraySeq(
true
,
false
,
true
)
@ parseFromString[
Seq[
Int
]
]
(
"1,2,3,4"
)
res100:
Seq[
Int
]
=
ArraySeq(
1
,
2
,
3
,
4
)
</> 5.49.scala
我们教会了编译器为任何类型 T
产生一个 StrParser
,只要它有一个可用的隐式 [
Seq[
T]
]
StrParser
。由于我们已经有了 [
T]
StrParser
,[
Int
]
StrParser
,以及 [
Boolean
]
StrParser
,[
Double
]
ParseSeq
自然支持了 StrParser
,[
Seq[
Int
]
]
StrParser
,以及 [
Seq[
Boolean
]
]
StrParser
。[
Seq[
Double
]
]
我们正在实例化的 StrParser
有一个解析方法,它接收一个参数 [
Seq[
T]
]
s
,返回一个 :
String
Seq
。正如我们在上述代码片段中所做的,我们只需要实现变换操作的必要逻辑。[
T]
类似我们定义
去解析 implicit
def
Seq
,我们同样可以解析元组。我们假设以下输入字符串用 [
T]
key
代表元组:=
value
implicit
def
ParseTuple[
T,
V]
(
implicit
p1:
StrParser[
T]
,
p2:
StrParser[
V]
)
=
new
StrParser[
(
T,
V)
]
{
def
parse(
s:
String
)
=
{
val
Array(
left,
right)
=
s.
split(
'='
)
(
p1.
parse(
left)
,
p2.
parse(
right)
)
}
}
</> 5.50.scala
我们定义出了 StrParser
,但要求类型 [
(
T,
V)
]
T
和 V
都有对应可用的 StrParser
。现在,我们可以把用
拼接的对值解析为元组:=
@ parseFromString[
(
Int
,
Boolean
)
]
(
"123=true"
)
res102:
(
Int
,
Boolean
)
=
(
123
,
true
)
@ parseFromString[
(
Boolean
,
Double
)
]
(
"true=1.5"
)
res103:
(
Boolean
,
Double
)
=
(
true
,
1.5
)
</> 5.51.scala
上述两个定义,
和 implicit
def
ParseSeq
,足以让我们解析元组序列和序列元组:implicit
def
ParseTuple
@ parseFromString[
Seq[
(
Int
,
Boolean
)
]
]
(
"1=true,2=false,3=true,4=false"
)
res104:
Seq[
(
Int
,
Boolean
)
]
=
ArraySeq(
(
1
,
true
)
,
(
2
,
false
)
,
(
3
,
true
)
,
(
4
,
false
)
)
@ parseFromString[
(
Seq[
Int
]
,
Seq[
Boolean
]
)
]
(
"1,2,3,4,5=true,false,true"
)
res105:
(
Seq[
Int
]
,
Seq[
Boolean
]
)
=
(
ArraySeq(
1
,
2
,
3
,
4
,
5
)
,
ArraySeq(
true
,
false
,
true
)
)
</> 5.52.scala
注意本例中,由于我们仅简单地分割输入字符串,所以我们还不能处理嵌套的 Seq
或者 嵌套元组。一个更具结构化的解析器可以毫无压力地处理这种案例,允许我们指定任意复杂的输出类型,并自动推断相应的解析器。我们将在 Chapter 8: JSON and Binary Data Serialization 使用支持这种技术的序列化库。[
Seq[
T]
]
绝大多数静态类型语言可以一定程度推断类型:没有直接注解类型的表达式,编译器也能够基于程序结构弄明白其类型。衍生物Typeclass正好相反:根据提供的类型,编译器推断必要的程序结构来提供我们所需的隐式值。
上述例子中,我们只需要定义如何处理基本类型 - StrParser
,[
Boolean
]
StrParser
,[
Int
]
StrParser
,[
Seq[
T]
]
StrParser
- 编译器就能够弄明白当我们需要一个 [
(
T,
V)
]
StrParser
时,该如何产生它。[
Seq[
(
Int
,
Boolean
)
]
]
本章,我们探索了更多Scala独一无二的特性。Case Class和模式匹配是我们日常使用到的,而传名参数,隐式参数,及Typeclass推断是更高级的工具,你可能只会在框架或库中使用它们。然而,正是这些特性造就了Scala的样子,提供了比绝大多数主流语言更优雅地解决困难问题的方式。
我们在本章中浏览了这些特性的基本动机和用例。本书剩余部分,你会通过动手完成更多用例来熟悉这些特性。
这是我们单独谈论Scala编程语言的最后一章节:后续章节会给你介绍更多更复杂的主题,比如操作系统的使用,远程服务的开发,以及第三方库。当你拓展视野,从学习Scala语言本身到需要用Scala解决真实世界的问题时,你已掌握的Scala语言基础会很好地帮助到你。
Exercise: Define a function that uses pattern matching on the Expr
s we saw earlier to
perform simple algebraic simplifications:
|
|
|
|
|
|
|
|
Exercise: Modify the
function earlier that takes a by-name parameter and
make it perform an exponential backoff, sleeping between retries, with a
configurable initial def
retrydelay
in milliseconds:
retry(
max =
50
,
delay =
100
/*milliseconds*/
)
{
requests.
get(
s"$httpbin/status/200,400,500")
}
</> 5.53.scala
See example 5.8 - BackoffExercise: Modify the typeclass-based parseFromString
method we saw earlier to take a
JSON-like format, where lists are demarcated by square brackets with
comma-separated elements. This should allow it to parse and construct
arbitrarily deep nested data structures automatically via typeclass inference:
@ parseFromString[
Seq[
Boolean
]
]
(
"[true,false,true]"
)
// 1 layer of nesting
res1:
Seq[
Boolean
]
=
List(
true
,
false
,
true
)
@ parseFromString[
Seq[
(
Seq[
Int
]
,
Seq[
Boolean
]
)
]
]
(
// 3 layers of nesting
"[[[1],[true]],[[2,3],[false,true]],[[4,5,6],[false,true,false]]]"
)
res2:
Seq[
(
Seq[
Int
]
,
Seq[
Boolean
]
)
]
=
List(
(
List(
1
)
,
List(
true
)
)
,
(
List(
2
,
3
)
,
List(
false
,
true
)
)
,
(
List(
4
,
5
,
6
)
,
List(
false
,
true
,
false
)
)
)
@ parseFromString[
Seq[
(
Seq[
Int
]
,
Seq[
(
Boolean
,
Double
)
]
)
]
]
(
// 4 layers of nesting
"[[[1],[[true,0.5]]],[[2,3],[[false,1.5],[true,2.5]]]]"
)
res3:
Seq[
(
Seq[
Int
]
,
Seq[
(
Boolean
,
Double
)
]
)
]
=
List(
(
List(
1
)
,
List(
(
true
,
0.5
)
)
)
,
(
List(
2
,
3
)
,
List(
(
false
,
1.5
)
,
(
true
,
2.5
)
)
)
)
</> 5.54.scala
A production-ready version of this parseFromString
method exists in
upickle
, which we will see in Chapter 8: JSON and Binary Data Serialization..
default.
read
Exercise: How about using typeclasses to generate JSON, rather than parse it? Write a
writeToString
method that uses a StrWriter
typeclass to take nested values
parsed by parseFromString
, and serialize them to the same strings they were
parsed from.
@ writeToString[
Seq[
Boolean
]
]
(
Seq(
true
,
false
,
true
)
)
res1:
String
=
"[true,false,true]"
@ writeToString(
Seq(
true
,
false
,
true
)
)
// type can be inferred
res2:
String
=
"[true,false,true]"
@ writeToString[
Seq[
(
Seq[
Int
]
,
Seq[
Boolean
]
)
]
]
(
Seq(
(
Seq(
1
)
,
Seq(
true
)
)
,
(
Seq(
2
,
3
)
,
Seq(
false
,
true
)
)
,
(
Seq(
4
,
5
,
6
)
,
Seq(
false
,
true
,
false
)
)
)
)
res3:
String
=
"[[[1],[true]],[[2,3],[false,true]],[[4,5,6],[false,true,false]]]"
@ writeToString(
Seq(
(
Seq(
1
)
,
Seq(
(
true
,
0.5
)
)
)
,
(
Seq(
2
,
3
)
,
Seq(
(
false
,
1.5
)
,
(
true
,
2.5
)
)
)
)
)
res4:
String
=
"[[[1],[[true,0.5]]],[[2,3],[[false,1.5],[true,2.5]]]]"
</> 5.55.scala
See example 5.10 - Serialize