《Scala函数式编程》中文版勘误

感谢林晴、史珩、shahito、乐乐Joker 以及其他匿名用户的反馈,勘误表如下(另外,原书作者另写了一本伴生书,对文章的错别字以及个别代码样例错误做了纠正,伴生书pdf可从作者网站免费下载:http://blog.higher-order.com/assets/fpiscompanion.pdf 为了保持一致,伴生书中修订的内容在中文版中并没有修改。伴生书中指出的错误并不多,也不影响本书阅读,主要是为书中的练习做解释)

页数:目录
 原文:1.3 引用透明、纯粹度以及替代模型
 修改:1.3 引用透明、纯粹性以及代换模型

页数:目录 XI
 原文:4 不是用异常来处理错误
 修改:4 不使用异常来处理错误

页数:目录 XII
 原文:第二部分  功能设计和组合子库
 修改:第二部分  函数式设计和组合子库

 原文:9.1 代数设计,走起
 修改:9.1 代数设计

页数:目录 XIII
 原文:第三部分  函数设计的通用结构
 修改:第三部分  函数式设计的通用结构

页数:目录 XV
 原文:14 本地影响和可变状态
 修改:14 局部作用和可变状态

 原文:14.3 纯粹是相对于上下文的
 修改:14.3 纯粹性是相对于上下文的

页数:原推荐序
 第一段中
 原文:函数式编程作为书题出现在Scala中是个有趣的现象。
 修改:《Scala函数式编程》是个有趣的书名。

 第二段中
 原文:它同时承认非纯粹函数和纯函数
 修改:它同时允许非纯粹函数和纯函数

 第五段中
 原文:从第一个原理扩展到
 修改:从首要原则扩展到

页数:第一部分 函数式编程介绍
 第一段中
 原文:我们以一个激进的前提开始读这本书
 修改:我们以一种激进的前提开始这本书

 原文:比如读取文件或修改内存时。
 修改:比如读取文件或修改内存。

 第二段中
 原文:并给你一些有益的理念。
 修改:让你对函数式编程的好处有些概念。

页数:8
 原文:引用透明与纯粹度
 修改:引用透明与纯粹性

 原文:1.3 引用透明、纯粹度以及替代模型
 修改:1.3 引用透明、纯粹性以及代换模型

 // 统一翻译为“代换模型”
 原文:我们称之为代替模型( substitution model )。
 修改:我们称之为代换模型( substitution model )。

 原文:其中一个例子的所有表达式都是引用透明的,可用替代模型来推导,
 修改:其中一个例子的所有表达式都是引用透明的,可用代换模型来推导,

 原文:让我们尝试在Scala解析器(也称作REPL,
 修改:让我们尝试在Scala解释器(也称作REPL,

页数:10
 原文:与之相反的是,替换模型则很容易推理
 修改:与之相反的是,代换模型则很容易推理

 原文:即使没有用过“替代模型”这一名词,
 修改:即使没有用过“代换模型”这一名词,

 原文:还对引用透明和代替模型进行了讨论,
 修改:还对引用透明和代换模型进行了讨论,

页数:22
 // andThan -> andThen
 原文:同时还提供了一个 andThan 方法, f andThan g 等价于 g compose f
 修改:同时还提供了一个 andThen 方法, f andThen g 等价于 g compose f

页数:23
 原文:对于像这样小的一行层序,还不算困难
 修改:对于像这样的一行小程序,还不算困难

页数:40
 原文:不是用异常来处理错误
 修改:不使用异常来处理错误

页数:41
 原文:让替代模型的简单推导无法适用
 修改:让代换模型的简单推导无法适用

页数:54
  原文(最下面的注释): 我们现在使用Scala标注库
  修改:我们现在使用Scala标准库

页数:57
 原文:如果表达式 f(x) 对所有的evaluates to bottom的表达是x,也是evaluates to bottom,那么f是严格求值的。
 修改:如果表达式f(x)对于所有evaluates to bottom的表达式x同样是evaluates to bottom的,那么f是严格求值的。

页数:67
 原文:目的是模拟投6面色子死亡法
 修改:目的是模拟投6面体骰子

页数:73
 原文:这是向右移的方式,
 修改:这是朝着正确方向前进,

 原文:用纯函数式API实现一个更加可测的死亡色子?
 修改:用纯函数式API实现一个更加可测的投骰子?

 原文:这里是一个死亡色子 rollDie 的实现,
 修改:这里是一个投骰子 rollDie 的实现,

页数:77
 原文:第二部分 功能设计和组合子库
 修改:第二部分 函数式设计和组合子库

页数:81
 原文:两边的表达式是无法实现平行执行的。
 修改:两边的表达式是无法实现并行执行的。

 注释2
 原文:并在衍生下一个并行计算之前等待前一个并行计算完成,这样的计算比较高效同时也保证串行执行。
 修改:并在衍生下一个并行计算之前等待前一个并行计算完成,这样的计算实际是串行的。

页数:90
 原文:(法则常常是从标识的具体例子中得出来的)
 修改:(法则常常是从恒等式的具体例子中得出来的)

 注释8
 原文:这里的标识我们想表达的是,在数学上指两个表达式相同或等价
 修改:这里的恒等式表达的是,在数学上指两个表达式相同或等价

页数:91
 //将 fascinating 翻译为了“醉了吧” 与整本书的语言风格不一致,删除。
 原文:醉了吧!从最后的法则可以看出对于map而言unit明显是个多余的细节。
 修改:从最后的法则可以看出对于map而言unit明显是个多余的细节。

 原文:由于我们得到的这个第二法则或定理是自由的,仅仅是因为map的参数态(parametricity),它有时也被称为自由定理(free theorem)。
 修改:由于我们得到的这个第二法则或定理是免费的,仅仅是因为map的参数态(parametricity),它有时也被称为免费定理(free theorem)。

 注释10
 原文:这和我们在代数方程式里做的简化过程一样。
 修改:这和我们在代数方程式里做的代换和简化过程一样。

 注释12 // 译注:在优化界有一个著名的定理叫“没有免费午餐定理”,这个论文的题目可能是想与之呼应
 原文:”自由定理”的观点来自于 Philip Wadler 的经典论文《Theorem for Free》(http://mng.bz/Z9f1)
 修改:”免费定理”的观点来自于 Philip Wadler 的经典论文《Theorem for Free》(http://mng.bz/Z9f1)

页数:103
 原文:某些情况下,Gen[A] 的空间(domain)足够小
 修改:某些情况下,Gen[A] 的定义域(domain)足够小

 注释2
 原文:这里的“空间”(domian)与函数空间一样
 修改:这里的“定义域”(domian)与函数定义域一样

页数:108
 // Refining 在编译器里更多翻译为”具化"
 原文:8.2.5 精炼 Prop 的数据类型
 修改:8.2.5 具化 Prop 的数据类型

页数:117
 注释13
 原文:回忆一下在第7章中我们曾介绍过的自由定理,
 修改:回忆一下在第7章中我们曾介绍过的免费定理,

页数:117
 // 风格不符
 原文:9.1 代数设计,走起
 修改:9.1 代数设计

页数:153
 注释3
 原文:因为借用了已经存在的数据抽象名称
 修改:因为借用了已经存在的数学抽象名称

页数:164
 // primitive翻译为原语,保持一致风格
 原文:State中(除了unit和flatMap)其他原始的操作
 修改:State中(除了unit和flatMap)其他原语操作

 原文:它们和monadic原始操作(unit和flatMap) 一起构成了State数据类型的所有操作。monad一般都是这样的,它们都包括unit和flatMap,并且每个monad又有自己额外的原始操作。
 修改:它们和monadic原语操作(unit和flatMap) 一起构成了State数据类型的所有操作。monad一般都是这样的,它们都包括unit和flatMap,并且每个monad又有自己额外的原语操作。

页数:172
 原文:Applicative构建了上下文自由的计算,
 修改:Applicative构建了上下文无关的计算,

页数:210
 原文:本地影响和可变状态
 修改:局部作用和可变状态

页数:212
 注释1
 原文:也无须每次在充分利用本地变更(local mutation)时使用。
 修改:也无须每次在充分利用局部变更(local mutation)时使用。

页数:214
 // primitive 统一翻译为原语
 原文:这里依旧采用组合子库加一些基元函数(primitive)的形式,其中关于可变内存单元的应有的基元函数有:
 修改:这里依旧采用组合子库加一些原语函数(primitive)的形式,其中关于可变内存单元的应有的原语函数有:

页数:217
 // primitive 统一翻译为原语
 原文:为此,我们需要先实现可变数组的基元组合子:
 修改:为此,我们需要先实现可变数组的原语组合子:

页数:218
 // 基元->原语,负责->复杂
 原文:有了这些基元函数,我们便可以实现更负责的数组函数了。
 修改:有了这些原语函数,我们便可以实现更复杂的数组函数了。

 原文:我们不如把这变成一个基元函数:
 修改:我们不如把这变成一个原语函数:

页数:219
 练习14.3
 原文:为 scala.collection.mutable.HashMap 实现一组最小的基元组合子。
 修改:为 scala.collection.mutable.HashMap 实现一组最小的原语组合子。

页数:240
 原文:链状混合(Zipping)是Tee特有的一种情况,
 修改:拉链式操作(Zipping)是Tee特有的一种情况,

页数:242
 原文:请用存在基元函数实现join,
 修改:请用已存在的原语函数实现join,

scala雾中风景(26): 变量查找的问题

在Java/Scala的一个方法里,存在与全局变量同名的局部变量的话将会覆盖这个全局变量,但当在这个方法局部变量定义之前就引用这个变量,在Java和Scala的编译器里给出了不同的实现,先看Java里:

➜  cat B.java
public class B {
    static String name = "noname";
    public static void main(String[] args) {
      System.out.println(name);
      String name = "wang";
    }
}

➜  java B
noname

在main方法里第一行引用的name是全局变量,在同名的局部变量定义之前它从全局查找这个变量。而在Scala里:

 ➜  cat A.scala
object A {
    val name: String = "noname"

    def main(args: Array[String]) {
        println(name)
        val name = "wang"
    }
}

 ➜  scalac A.scala
A.scala:5: error: forward reference extends over definition of value name
    println(name)
            ^
one error found

编译时错误,它优先从方法内部的局部变量表类查找了。我更接受Java里的方式,而且这种方式已经深入人心了,对Scala为何这样做我不清楚是出于什么考虑,是有意这样还是实现上的bug?

scala雾中风景(25): try-finally表达式的类型推导

一段实际代码简化后如下:

class A {
  def foo():String = {
    try{
      Thread.sleep(1000)
    }finally{
      "ok"
    }
  }
}

本来期望这个foo方法返回finally块中的”ok”,但编译的时候却给出了类型不匹配的错误:

 ➜  scalac A.scala
A.scala:4: error: type mismatch;
 found   : Unit
 required: String
      Thread.sleep(1000)
                  ^

按说scala类型推断这么强大,不应该推断不出最终的返回值类型,从编译器的错误来看似乎它非要求在try代码块里最后一行表达式必须也是String类型的值,为什么finally里的表达式没有参与类型推断呢?

把上述代码稍作改动,在try代码块里明确的给出一个String结果

def foo():String = {
    try{
        Thread.sleep(1000)
        "res"
    }finally{
        "ok"
    }
}

再编译一下,却给出了一行警告:

 ➜  scalac A.scala
A.scala:7: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
    "ok"
    ^
one warning found

分析一下为什么编译器认为这句表达式”does nothing”,而不把它当作返回值对待:

 ➜  scalac -Xprint:typer A.scala
 ...

def foo(): String = try {
  java.this.lang.Thread.sleep(1000L);
  "res"
} finally {
  "ok";
  ()
}

看到编译器在finally块的”ok”表达式后边自行增加了一个返回Unit类型的值(),看上去编译器认为finally块里的逻辑是一个“procedure”,一定要满足Unit

从scala语言规范来看,try-catch-finally表达式也是有返回值的,且返回值主要是取决于trycatch里的最后一行表达式,而finally被认为是做一些收尾的工作的,不应该在里面去改变返回结果。

具体到这个案例,foo方法声明的返回值类型是Stringfoo方法体里的try-finally表达式的值就是最终的返回值,而try-finally表达式的值是取决于try代码块里的最后一行表达式,而非finally块里的。

看几个例子:

scala> val a = try { 100 } finally { 200 }
<console>:7: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
   val a = try { 100 } finally { 200 }
                                 ^
a: Int = 100

上面try-finally语句的结果是try里的值。

scala> val a = try { throw new Exception() } catch { case e:Exception => 200 }
a: Int = 200

上面try里发生了异常,最终的结果是catch里的。

scala> val a = try { throw new Exception() } catch { case e:Exception => 200 } finally { 300 }
<console>:7: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
   val a = try { throw new Exception() } catch { case e:Exception => 200 } finally { 300 }
                                                                                     ^
a: Int = 200

上面finally里的表达式并不会被当作最终返回值。

当然,在finally块里是可以使用return关键字的,但return关键字在这里并不能改变try-finally表达式的结果类型,我们对原代码增加return再编译:

def foo():String = {
    try{
        Thread.sleep(1000)
    }finally{
      return "ok"
    }
}

 ➜  scalac A.scala
A.scala:4: error: type mismatch;
 found   : Unit
 required: String
      Thread.sleep(1000)
                  ^
one error found

依然编译错误,注意return是面向method的,属于流控层面,并不影响表达式的类型推断。因为在foo方法里try-finally就是最后一句表达式,所以编译器要求这句表达式的类型必须也满足foo的返回值类型签名。如果try-finally不是最后一句,就没有这个约束了,比如:

def foo():String = {
    try{
        Thread.sleep(1000)
    }finally{
      return "ok"
    }

    "no"
}

上面对foo方法在最后一行增加了一句返回”no”的表达式,使得前边的try-finally表达式类型推导不受方法签名的约束,编译可以通过了。当然这个代码逻辑肯定不会走到那里,我更希望编译器给出代码不可达的警告。

如果打开typer-debug编译选项,可以看到编译器总会期待方法里的最后一个表达式满足方法返回值类型,如果最后的这个表达式又是由多个更小粒度的表达式组合成的(比如这个try-finally,我们暂称它为大表达式),则进一步对这个大表达式拆分推导,约束其中的决定整个大表达式类型的小表达式也必须符合方法的返回类型,对于try-finally这个大表达式来说就是其中try块里的最后一行表达式。

对于try-catch-finallyfinally,编译器总是预期它里面的表达式类型为Unit,所以如果在里面的最后一条语句不是一个Unit类型的值,编译会自动给你加上。

注意,return ok这句表达式的类型是Nothing,不要混淆方法返回值类型和表达式自身类型。return, throw等跟流控相关的表达式都是Nothing,它可以满足任何类型,自发可以符合finally里的Unit预期。

其实这个问题是scala类型推导实现的问题,我们期望它更聪明一些,比如:

scala> def bar:String = { return "A";  "B"  }
bar: String

在Java里编译器会报错后边的语句不可达,但Scala里却编译通过。虽然后边的表达式没有意义,不会走到那儿,但并不意味着你能给出任意的值:

scala> def bar:String = { return "A";  200 }
<console>:7: error: type mismatch;
 found   : Int(200)
 required: String
   def bar:String = { return "A";  200 }

尽管后边的表达式不会被执行到,但它在编译时参与类型推导,因为对于该方法来说 { return "A"; 200 }整体是一个大表达式也必须满足String类型才行。

scala里模拟javascript/python里的生成器的效果

上一篇关于yield的字面含义,基本有答案了。对于python和javascript不太熟悉,这两个语言里yield都是配合生成器而使用的。比如下面这段 javascript 的例子从mozilla的网站上看到的,稍加改造:

➜  cat test.js

function* foo() {
  var index = 0;

  while (index <= 2 ) {
    console.log("here" + index);
    yield index++;
  }
}

var it = foo();

console.log("generator ready");

console.log(it.next());
console.log(it.next());
console.log(it.next());

它的输出结果是

➜  node test.js
generator ready
here0
{ value: 0, done: false }
here1
{ value: 1, done: false }
here2
{ value: 2, done: false }

在Scala里yield并不等效于javascript/python里的效果,要等效的话,必须通过lazy, Stream等延迟计算特性来实现:

➜  cat Test.scala
object Test {
  def main(args: Array[String]) {

    lazy val c = for( i <- (0 to 2).toStream ) yield {
      println("here" + i);
      i+1
    }

    println("ready")

    for( n <- c) {
      println(n)
    }
  }
}   

运行结果:

➜  scala Test.scala
ready
here0
1
here1
2
here2
3