Scala谜题

978-7-115-46007-3
作者: 【美】Andrew Phillips(菲利普斯) Nermin Šerifović(萨尔法维克 )
译者: 包春霞冷钰冰
编辑: 陈冀康武晓燕

图书目录:

详情

本书整合了众多的Scala代码示例,深入解密Scala。书中不仅介绍了Scala语言,还介绍了编译器。本书通过有趣的方式带领读者学习并深入理解和掌握Scala。每一个谜题都可以丰富读者的知识,并能够更深入地了解Scala。本书将帮助读者快速提高,并且更好地使用Scala语言。

图书摘要

版权信息

书名:Scala谜题

ISBN:978-7-115-46007-3

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Andrew Phillips Nermin Šerifović

  译    包春霞 冷钰冰

  责任编辑 陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Simplified Chinese translation copyright ©2017 by Posts and Telecommunications Press

ALL RIGHTS RESERVED

Scala Puzzlers:The fun path to deeper understanding, by Andrew Phillips and Nermin Šerifović ISBN 9780981531670

Copyright © 2016 by Artima, Inc.

本书中文简体版由Artima, Inc.授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


Scala是一种多范式的编程语言,其设计初衷是要整合面向对象编程和函数式编程的各种特性。

本书整合了众多典型的Scala代码示例,深入解密Scala。书中不仅介绍了Scala语言,还介绍了编译器。本书通过有趣的方式带领读者学习并深入理解和掌握Scala。全书共有36个谜题,每一个谜题都可以丰富读者的知识,并能够让读者更深入地了解Scala。

本书适合于对Scala感兴趣的开发者、对JVM平台上的语言以及函数式编程感兴趣的程序员阅读。


谜题很有趣。我还记得第一次遇到Java语言的“谜题”的情景。Neal Gafter给我看了他和Josh Bloch刚收集的一道Java谜题。那时候,Neal正在维护我编写的javac编译器。这道题太难了,我猜错了几次,让Neal乐坏了。

很高兴现在有一本书能在Scala中延续谜题的传统。本书将为读者展示36个谜题,之所以称为谜题,是因为它们会产生令人意外的结果,有特性间的交互作用或者有不同于表面的代码运行结果。这些谜题是从Scala社区几年来广泛的输入中收集而来的。

Andrew和Nermin提炼出每个谜题的本质,使其易于理解。在享受了从一系列选项中选出正确答案的乐趣之后,每道题后面会告诉你为什么会这样,什么原因导致了这个令人意外的结果?这正是本书真正的亮点,因为它对观察到的程序行为的深层原理进行了清晰地讲解。我特别喜欢本书的一点是这些讲解总是给你带来全新的视角。它们不仅告诉你令人意外的行为的趣事,而且集Scala深入讲解于一体。这样,谜题就会有助于你更深入地理解这门语言。

希望你能像我一样觉得阅读本书并试着解决这些谜题很有趣。是的,有些谜题我还不能解决。

Martin Odersky

Scala之父

2014.5.26


感谢每一位参与者,无论是提供了谜题代码,还是写了注释或建议,或者仅仅是传播了文字。感谢你们使本书的编写和Scala谜题网站scalapuzzlers.com的制作得以实现。

真挚地感谢我们的编辑Jessica Kerr、Bill Venners和Theresa Gonzalez,感谢他们的探索和对本书的全面评审,感谢Darlene Wallach和George Berger排版并采用发布系统来处理这本书。

还要特别感谢Scala谜题scalapuzzlers.com网站的所有贡献者:Dominik Gruntz、A. P. Marki、Simon Schäfer、Konstantine Golikov、Seth Tisue、Daniel C. Sobral、Luc Bourlier、Vassil Dichev和Andraž Bajt。你们的贡献为本书提供了基础和灵感。

也要对那些指出错误和提出改进建议的读者表示感谢:Cay Horstmann、Harish Hurchurn、Marcin Kubala、Edward G. Prentice,Alex Varju。特别感谢Dominik Gruntz的全面评审并提出许多有用的建议。


我的母亲Karin如涓涓细流般美妙的文笔熏陶我、影响我写成本书;我的两个耐心的朋友Libby和Bill Venners,她们一直在用Scalac编译程序,要不是她们,我就不会认识Scala。

——A.P.

我出色的妻子Džana毫无保留的支持,帮我完成了另一个副项目。我的两个调皮的儿子Immy和Rayan,他们很好奇我是否在写一本“真正”的、以“从前”开始,以“结束”结尾的书。我敬爱的父母,Nad和Sabrija,他们在孩子的教育上投入大量的时间。

——N.Š.


让代码做我们希望它做的事,是作为一个开发者的基本目的。所以没有什么比我们自认为理解的一段代码表现出与我们期望相反的行为更迷人、更重要的了。

本书汇集了一些Scala的例子。这不仅是以一种寓教于乐的方式来更好地理解这门语言,而且帮助你认识许多反直观的雷区和陷阱,防止它们在生产系统中干扰到你。

全书共有36章,具体内容如下。

本书中的谜题并非按特定顺序编写。你可以随机打开一个谜题阅读,会与从头到尾地读一样轻松。

如果你对Scala语言的特定方面感兴趣并且在寻找相关的谜题,本书最后的主题索引正是为你准备的。我们尽量根据读者要探索的主题对谜题进行了分类。

谜题中所有的代码例子都要用2.11 Scala REPL[1]解释执行,最近的一些变更,例如一些不支持的写法会让程序行为与Scala 2.10.X版本稍微不同,我们已经增加了注释或注脚。

尽管本书经过了严格评审,但错误还是不可避免的。如果你发现任何错误,请通过http://booksites.artima.com/scalapuzzlers/errata告诉错误所在的页码。

本书同时提供印刷版书籍和PDF格式电子书。电子书并不是本书印刷版的简单的电子版复制。在保持与印刷版内容相同的同时,电子书还经过仔细设计和优化以适应用计算机屏幕阅读。

首先要注意的是,电子书里大部分引用是超链接的形式。如果你选择进入某章、某个图形或词汇条目的超链接,PDF查看器会立即将你带到选择的条目而不必翻来翻去地寻找。

此外,电子书的每页底部是一些导航链接。“封面”“概述”“目录”链接将你带到本书的开头;“索引”链接将你带到本书最后的索引部分;“讨论”链接将你带到论坛,在那里你可以与其他读者、作者和更大的Scala社区讨论问题。如果你发现一个拼写错误或者某些你认为能解释得更好的地方,请单击“建议”链接进入在线Web应用,在这里你可以给作者提供反馈。

尽管电子版与印刷版有相同的页面,但我们已经删除了电子书的空白页,并对剩下的页面重新编排了页码。各页的页码不同以便当你只打印本书的一部分时很容易决定PDF的页数。电子书的页码已经严格按照PDF阅读器计数方式进行了编排。

一个术语首次使用的时候是斜体字。小的代码样例在同一行里用单间隔字体,例如x + 1。较大的代码样例放在单行间隔的引用块中,如下所示:

def hello() {
  println("Hello,world!")
}

当出现交互式Shell时,Shell返回结果用亮字体显示:

scala> 3 + 4
res0:Int = 7

[1] 从命令行输入“Scala”来启动REPL(读—评估—打印—循环,Read-Evaluate-Print-Loop,REPL)。


1.使用占位符  1

2.初始化变量  5

3.成员声明的位置  9

4.继承  14

5.集合操作  21

6.参数类型  24

7.闭包  29

8.Map表达式  33

9.循环引用变量  37

10.等式的例子  44

11.lazy val  51

12.集合的迭代顺序  54

13.自引用  58

14.Return语句  62

15.偏函数中的_  67

16.多参数列表  73

17.隐式参数  78

18.重载  83

19.命名参数和缺省参数  88

20.正则表达式  93

21.填充  97

22.投影  101

23.构造器参数  106

24.Double.NaN  111

25.getOrElse  116

26.Any Args  120

27.null  124

28.AnyVal  129

29.隐式变量  135

30.显式声明类型  141

31.View  145

32.toSet  148

33.缺省值  154

34.关于Main  159

35.列表  165

36.计算集合的大小  169


Scala特别强调要书写简单、简洁的代码。匿名函数的语法arg => expr,使它很容易用最小模板构建函数字面量,甚至函数由多个语句组成时也一样可以。

用有自解释参数的函数还可以做得更好,而且还可以用占位符语法。占位符语法可以省去参数声明。例如:

List(1,2).map { i => i + 1 }

用占位符语法,就变成:

List(1,2).map { _ + 1 }

以下两个语句是等价的:

scala> List(1,2).map { i => i + 1 }
res1:List[Int] = List(2,3)

scala> List(1,2).map { _ + 1 }
res0:List[Int] = List(2,3)

如果你给以上简单的例子增加调试语句会如何呢?这有助于你理解函数是什么时候应用的。让我们看看在REPL中执行以下代码会是什么结果。

List(1,2).map { i => println("Hi");i + 1 }
List(1,2).map { println("Hi"); _ + 1 }

1.打印出:

Hi
List[Int] = List(2,3)
Hi
List[Int] = List(2,3)

2.打印出:

Hi
Hi
List[Int] = List(2,3)
Hi
Hi
List[Int] = List(2,3)

3.打印出:

Hi
Hi
List[Int] = List(2,3)
Hi
List[Int] = List(2,3)

4.第一个语句打印出:

Hi
Hi
List[Int] = List(2,3)

第二个语句编译失败。

你无需关心编译错误,因为代码编译没有问题。然而程序并没有表现出你期望的结果。正确的答案是3:

scala> List(1, 2).map { i => println("Hi"); i + 1 }
Hi
Hi
res23:List[Int] = List(2,3)

scala> List(1, 2).map { println("Hi"); _ + 1 }
Hi
res25:List[Int] = List(2,3)

怎么会是这样呢?如果这个有显式参数的函数打印“Hi”两次,因为它会对列表中的每个元素都调用一次函数,那么为什么使用占位符语法的函数不能有同样的结果呢?

因为匿名函数常常被当作参数传递,在代码中往往会看到它们在花括号{ ... }里,就很容易认为这些花括号表示一个匿名函数。但是,实际上它们只是界定了一个块表达式,一个或多个表达式最后决定了这个块的结果。

两个代码块的解析方式决定了它们有不同的行为。第一个语句{ i => println("Hi"); i + 1 }被当成一个arg => expr形式的函数字面量表达式,这里的表达式是块println("Hi"); i + 1。因为println语句是函数体的一部分,所以每次调用函数就要执行一次。

scala> val printAndAddOne =
         (i: Int) => { println("Hi"); i + 1 }
printAndAddOne: Int => Int = 

scala> List(1, 2).map(printAndAddOne)
Hi
Hi
res29: List[Int] = List(2, 3)

第二个表达式中,代码块被认为是println("Hi")``和``_+ 1两个表达式。当这个代码块执行的时候,将最后一个表达式(便利性所需的函数类型,Int => Int)传递给map。其中的println语句不是函数体的一部分,它是在map的参数评估时被调用的,而不是作为map的一部分执行。

scala> val printAndReturnAFunc =
         { println("Hi"); (_: Int) + 1 }
Hi
printAndReturnAFunc: Int => Int = 

scala> List(1, 2).map(printAndReturnAFunc)
res30: List[Int] = List(2, 3)

这里学到的关键一点是:用占位符语法定义的匿名函数的范围只延伸到含有下划线(_)的表达式。这不同于常规的匿名函数,常规的匿名函数的函数体是包含从箭头标识符(=>)一直到代码块结束的所有代码。请看下面这个例子:

scala> val regularFunc =
         { a: Any => println("foo"); println(a); "baz" }
regularFunc: Any => String = 

scala> regularFunc("hello")
foo
hello
res42: String = baz

而使用占位符语法的函数被封闭在它自己的代码块里。例如,下面两个函数是等价的:

scala> val anonymousFunc =
         { println("foo"); println(_: Any); "baz" }
foo
anonymousFunc: String = baz

scala> val confinedFunc =
         { println("foo"); { a: Any => println(a) }; "baz" }
foo
confinedFunc: String = baz

Scala鼓励简洁的代码,但太简洁时就会出现这样的情况。使用占位符语法时一定要注意由它所创建的函数范围。


Scala提供了几种便利方法可以初始化多个变量。有时候,这些方法会带来意想不到的结果。

在REPL中执行以下代码会是什么结果呢?

var MONTH = 12; var DAY = 24
var (HOUR, MINUTE, SECOND) = (12, 0, 0)

1.打印出:

MONTH:Int = 12
DAY:Int = 24
HOUR:Int = 12
MINUTE:Int = 0
SECOND:Int = 0

2.两个语句都编译失败。

3.第一个语句打印出:

MONTH:Int = 12
DAY:Int = 24

第二个语句抛出运行时异常。

4.第一个语句打印出:

MONTH:Int = 12
DAY:Int = 24

第二个语句编译失败。

你可能会想起关于大写变量和常数值的什么信息,怀疑这两个语句真能有一个会编译通过吗?如所发生的那样,第一行编译通过,第二行编译失败。正确的答案是4:

scala> var MONTH = 12; var DAY = 24
MONTH: Int = 12
DAY: Int = 24

scala> var (HOUR, MINUTE, SECOND) = (12, 0, 0)
:11: error: not found: value HOUR
       var (HOUR, MINUTE, SECOND) = (12, 0, 0)
            ˆ

:11: error: not found: value MINUTE
       var (HOUR, MINUTE, SECOND) = (12, 0, 0)
                  ˆ

:11: error: not found: value SECOND
       var (HOUR, MINUTE, SECOND) = (12, 0, 0)

Scala会让你对简单的单值赋值的val和var使用大写变量名,如例子中的MONTH和DAY。

当第二条语句用大写变量名技巧性地给多变量赋值的时候,这种技巧就引起了程序例外,因为多变量赋值是基于模式匹配的,而在一个模式匹配中,以大写字母开头的变量有着特别的含义:它们是静态标识符。

静态标识符是用来匹配常量的:

scala> final val TheAnswer = 42
scala> def checkGuess(guess: Int) = guess match {
 case TheAnswer => "Your guess is correct"
 case _ => "Try again"
       }
scala> checkGuess(21)
res8:String = Try again

scala> checkGuess(42)
res9:String = Your guess is correct

相反,小写变量定义的是变量的模式,这个模式会给变量赋值。

scala> var (hour, minute, second) = (12, 0, 0)
hour: Int = 12
minute: Int = 0
second: Int = 0

在我们的代码例子中,原意并不是对变量赋值,而是要匹配常量值。

如果你正在试图使用大写变量名,在极其偶然的情况下,恰巧匹配了范围内的值(在大型程序中常用的名字),模式匹配能编译成功,但成功还是失败取决于是否有值与之匹配:

val HOUR = 12; val MINUTE, SECOND = 0;

scala> var (HOUR, MINUTE, SECOND) = (12, 0, 0)

val HOUR = 13; val MINUTE, SECOND = 0;

scala> var (HOUR, MINUTE, SECOND) = (12, 0, 0)
scala.MatchError: (12,0,0) (of class scala.Tuple3)
  ...

注意,即便是第一种情况匹配成功,但实际上也没有变量被赋值:按照定义,在模式匹配时绝不会给静态标识符赋值。总之,最好什么都别发生,否则就会得到一个运行时例外。而这两种结果都不是我们想要的。

把小写变量放在单引号内时也能当做静态标识符使用。但要把它当作常量对待,这种情况就必须是val。

final val theAnswer = 42
def checkGuess(guess: Int) = guess match {
 case `theAnswer` => "Your guess is correct"
 case _ => "Try again"
}

scala> checkGuess(42)
res0: String = Your guess is correct

var theAnswer: Int = 42 // not a val, and not final either

scala> def checkGuess(guess: Int) = guess match {
 case `theAnswer` => "Your guess is correct"
 case _ => "Try again"
       }
:9: error: stable identifier required, but
  theAnswer found.
         case `theAnswer` => "Your guess is correct"

用大写名的var是没有考虑Scala的最佳实践,一般不太可能会遇到这样的意外:var用小写(最好完全避免使用大写!),常量用大写。正如《Scala语言规范》所描述的那样,还应将常量声明为final[1],这可以阻止子类重载它们,且当编译器能内联这些常量时还能得到额外的性能优点。

仅对常量使用大写的变量名。

[1] Odersky,《Scala语言规范》,4.1节。[Ode14]


相关图书

Rust游戏开发实战
Rust游戏开发实战
仓颉编程快速上手
仓颉编程快速上手
深入浅出Go语言编程从原理解析到实战进阶
深入浅出Go语言编程从原理解析到实战进阶
JUnit实战(第3版)
JUnit实战(第3版)
Go语言编程指南
Go语言编程指南
Scala速学版(第3版)
Scala速学版(第3版)

相关文章

相关课程