标签归档:scala

scala雾中风景(28): private?public?

记录前些天遇到的一个问题,scala里protectedprivate修饰的方法可能在编译为class时变成了public,这已经不是第一次遇到,最早遇到是在写一个java子类时要覆盖一些父类方法,父类是scala写的一个trait,里面的方法修饰为protected,当时IDE提示我override的方法必须声明为public感到奇怪反编译了一下父trait果然被声明为了public

而这次遇到的稍有不同,跟继承没有关系,用下面的demo举例:

 ➜  cat A.scala
class A {
  private[this] def foo() = {
    List(1,2,3).map(i => bar(i))
  }

  private[this] def bar(i:Int):String = {
    "str:" + i
  }
}

当我们编译上面类之后,里面的foobar方法的修饰符最终在class里会有所不同,反编译后可看到bar修饰符变成了public:

 ➜  cfr-decompiler A
 ...
 public class A {
    private List<String> foo() {
        return (List)List..MODULE$.apply((Seq)Predef..MODULE$.wrapIntArray(new int[]{1, 2, 3})).map((Function1)new scala.Serializable(this){
            public static final long serialVersionUID = 0;
            private final /* synthetic */ A $outer;

            public final String apply(int i) {
                return this.$outer.A$$bar(i);
            }
        }, List..MODULE$.canBuildFrom());
    }

    public String A$$bar(int i) {
        return new StringBuilder().append((Object)"str:").append((Object)BoxesRunTime.boxToInteger((int)i)).toString();
    }
}

终归scala在jvm上要做一些妥协,按上面的实现,foo里面以闭包的方式使用bar的时候,如果保持scala private[this]的控制粒度,底层的匿名类其实已经无法访问bar了。所以scala在编译器的explicitouter环节做了一些向现实妥协的事情

 ➜  scalac -Xshow-phases
    phase name  id  description
    ----------  --  -----------
        parser   1  parse source into ASTs, perform simple desugaring
         namer   2  resolve names, attach symbols to named trees
packageobjects   3  load package objects
         typer   4  the meat and potatoes: type the trees
        patmat   5  translate match expressions
superaccessors   6  add super accessors in traits and nested classes
    extmethods   7  add extension methods for inline classes
       pickler   8  serialize symbol tables
     refchecks   9  reference/override checking, translate nested objects
       uncurry  10  uncurry, translate function values to anonymous classes
     tailcalls  11  replace tail calls by jumps
    specialize  12  @specialized-driven class and method specialization
 explicitouter  13  this refs to outer pointers
       erasure  14  erase types, add interfaces for traits
   posterasure  15  clean up erased inline classes
      lazyvals  16  allocate bitmaps, translate lazy vals into lazified defs
    lambdalift  17  move nested functions to top level
  constructors  18  move field definitions into constructors
       flatten  19  eliminate inner classes
         mixin  20  mixin composition
       cleanup  21  platform-specific cleanups, generate reflective calls
    delambdafy  22  remove lambdas
         icode  23  generate portable intermediate code
           jvm  24  generate JVM bytecode
      terminal  25  the last phase during a compilation run

在这个阶段,当编译器发现一些private的方法会被内部类访问的话,就删除这些private修饰符:

 ➜  scalac -Xprint:explicitouter A.scala
[[syntax trees at end of             explicitouter]] // A.scala
package <empty> {
  class A extends Object {
    def <init>(): A = {
      A.super.<init>();
      ()
    };
    private[this] def foo(): List[String] = immutable.this.List.apply[Int](scala.this.Predef.wrapIntArray(Array[Int]{1, 2, 3})).map[String, List[String]]({
      @SerialVersionUID(value = 0) final <synthetic> class $anonfun extends scala.runtime.AbstractFunction1[Int,String] with Serializable {
        def <init>($outer: A.this.type): <$anon: Int => String> = {
          $anonfun.super.<init>();
          ()
        };
        final def apply(i: Int): String = $anonfun.this.$outer.bar(i);
        <synthetic> <paramaccessor> <artifact> private[this] val $outer: A.this.type = _;
        <synthetic> <stable> <artifact> def $outer(): A.this.type = $anonfun.this.$outer
      };
      (new <$anon: Int => String>(A.this): Int => String)
    }, immutable.this.List.canBuildFrom[String]());
    
    final def bar(i: Int): String = "str:".+(i)
  }
}

上面barprivate[this]在这个阶段被删除,而scala不同于java,缺省就是public,最终在class里变成了public

final object?

使用final修饰object的场景极少见,需要显式打开-Yoverride-objects编译选项才行:

 ➜  scala -Yoverride-objects
Welcome to Scala version 2.11.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_51).
Type in expressions to have them evaluated.
Type :help for more information.

scala> class A { object B }
defined class A

scala> class C extends A  { override object B {}  }
defined class C

如果A内部对object B使用final修饰了,子类C就不能覆盖这个object,不过它的意义是什么?这里object B是一个module,要解释object为何被当作module来设计,需要整理一下,等有时间再说。

PS,我刚发现2.11版本之后repl下,当你定义一个object时,提示已经不同了,在2.11版本之前,repl下会显示 "defined module XXX", 而 2.11 里已经变成了 "defined object XXX",可能隐含着设计者对module(早期scala中module应该是借鉴ML语言的module)这个术语可能存在理解不一致的担心,所以不再使用这个名词(只是我的猜测)。

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

感谢 shuai.xie 提出的这段漏掉的内容,这里补充一下。

这里b的类型声明并不是必须的。因为我们已经告诉Scala返回类型为B => C,Scala会从上下文获知b的类型,方法的实现部分只需要写为 b => f(a,b)就可以了。如果Scala能够推断出函数字面量的类型,就可以省略掉它的类型声明。

相关阅读:《Scala函数式编程》中文版勘误

scala雾中风景(27): lambda表达式里的return是抛异常实现的

尽管Scala和其他函数式编程都不太鼓励使用return,但我不喜欢太多缩进的代码,所以很少用较多的逻辑嵌套(除非是那种非常对称的嵌套),而是喜欢将不满足条件的先return掉。最近遇到一个scala里的流控陷阱,即在lambda里的return背后实际是通过特定的异常来实现的。

对于流控和异常捕获之前也遇到过其他陷阱,但稍不留意仍可能重犯,比如下面这段代码:

try {
    ...
    for (topicMeta <- resp.topicsMetadata; partMeta <- topicMeta.partitionsMetadata) {
      if (topicMeta.topic == p.topic && partMeta.partitionId == p.partition) {
        redisClient.hset(key, field, node.host + ":" + node.port)
        return
      }
    }
} catch {
    case e: Throwable =>
    ...
}

它是一段在actor里的逻辑,因为不希望非预期异常导致这个actor重启,所以是对Throwable进行的捕获,然而运行时竟捕获到了scala.runtime.NonLocalReturnControl$mcV$sp这样的异常。for语法糖容易让人忘记它里面的操作可能是一段匿名函数,简化一下这个例子:

➜  cat Test.scala
object Test {
    def main(args: Array[String]) {
        val list = List("A", "B", "C")
        for (e1 <- list) {
            if (e1 == "C") {
                println("ok, do something.")
                return
            }
        }
    }   
}

看看它编译的中间环节:

➜  scalac -Xprint:explicitouter Test.scala
[[syntax trees at end of             explicitouter]] // Test.scala
package <empty> {
object Test extends Object {
def <init>(): Test.type = {
  Test.super.<init>();
  ()
};
def main(args: Array[String]): Unit = {
  <synthetic> val nonLocalReturnKey1: Object = new Object();
  try {
    val list: List[String] = immutable.this.List.apply[String](scala.this.Predef.wrapRefArray[String](Array[String]{"A", "B", "C"}));
    list.foreach[Unit]({
      @SerialVersionUID(value = 0) final <synthetic> class $anonfun extends scala.runtime.AbstractFunction1[String,Unit] with Serializable {
        def <init>(): <$anon: String => Unit> = {
          $anonfun.super.<init>();
          ()
        };
        final def apply(e1: String): Unit = if (e1.==("C"))
          {
            scala.this.Predef.println("ok, do something.");
            throw new scala.runtime.NonLocalReturnControl$mcV$sp(nonLocalReturnKey1, ())
          }
        else
          ()
      };
      (new <$anon: String => Unit>(): String => Unit)
    })
  } catch {
    case (ex @ (_: scala.runtime.NonLocalReturnControl[Unit @unchecked])) => if (ex.key().eq(nonLocalReturnKey1))
      ex.value$mcV$sp()
    else
      throw ex
  }
}
}
}

很明显return在嵌套的匿名函数里是无法跳出外层函数的,所以编译器通过抛出 scala.runtime.NonLocalReturnControl 异常来实现跳出最外层。所有的lambda中使用return都是如此:

def main(args: Array[String]) {
    val lambda: String => Unit =
        (str: String) => { if ("hit" == str) return } //跳出main

    try {
        lambda("hit")
    } catch {
        case e: Throwable => //scala.runtime.NonLocalReturnControl
        e.printStackTrace()
    }
}

还是要注意对Throwable的捕获,不要干扰流控逻辑:

try{
    ...
}catch {
case e0: ControlThrowable => throw e0 // 不要干预流控的异常
case e1: Throwable => e1.printStackTrace
}   

《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       

Scala函数式编程中文版已上架

《Scala函数式编程》一书中文版已经可以在京东亚马逊上购买。因为相对小众领域,推荐对Scala或函数式感兴趣的人,尽快收一本,免的以后买不到,就像以前冰河翻译的那本《Practical Common Lisp》早已无法通过正常渠道购买。当然翻译的内容也难免会有问题,看到错别字或不顺畅的语句,或有些术语有更好的叫法的肯请指出来。

这里也把杨博和连城的推荐序转发一下,感谢他们为中文版写的推荐。

推荐序1

我可能是全中国程序员圈子里最不适合给《Scala函数式编程》写序的人。三年前我写过《Scala七大死穴》,算是把Scala批判了一番。前几天我则在准备 ThoughtWorks 咨询师大会上的讨论话题《没有函数的函数式编程》,又要杯葛函数式编程的样子。看起来,我无论对Scala还是对函数式编程,都没什么好评嘛。宏江莫不是疯了,居然要我来写序?

等等,事情似乎不是这样。最近几年, ThoughtWorks 的客户在越来越多的项目中采用了 Scala 技术栈,ThoughtWorks 也孵化出了若干基于 Scalaz 的开源项目。 我本人也在这些项目中起到了一些作用。为什么我会做这些“口嫌体正直”的事呢?这得从十年前说起。

我最早是在C++中开始接触到函数式编程的概念。C++ 和 Scala 一样,也是一门多范式语言。 C++ 的标准库 和 Boost 都提供了许多函数式编程的设施。但是,在我职业生涯初期,给我留下深刻印象的函数式编程库要数 Boost.Egg 。利用 Boost.Egg ,你可以写出 my_list|filtered(&is_not_X)|filtered(&is_not_Y) 这样的代码。你会注意到这种用法和 Scala 标准库非常相像,它大致相当于 Scala 的 myList.filter(isNotX _).filter(isNotY _), 这种 filter 的用法,本书第 5 章“严格求值和惰性求值”中也有讲解。

Boost.Egg 的另一个特点是“非侵入”,比如上例的 filtered 函数,本身并不是 my_list的成员。相反,我们通过重载 | 运算符给原有的类型添加新功能。这种做法在 Scala 里面相当于隐式转换,本书 第9章“语法分析器组合子”中提供的例子正是利用隐式转换,给字符串添加了中缀操作符。虽然 Boost.Egg 没能流行起来,但对我个人而言很重要,因为它很大程度塑造了我对代码的品位。有趣的是,Boost.Egg 的作者 Shunsuke Sogame 近年来的开源项目,都是些 Scala 项目,可能这也是因为 C++ 和 Scala 非常相似的缘故吧。

另一个对我代码品位影响很大的技术是 Lua 中的协程 (coroutine)。 Lua的作者 Roberto Ierusalimschy 把协程称为“单趟延续执行流”(One-shot continuation)。有了协程或者延续执行流,程序员可以手动切换执行流,不再需要编写事件回调函数,而可以编写直接命令式风格代码但不阻塞真正的线程。我的前东家网易在开发游戏时,会大量使用协程来处理业务逻辑,一个游戏程序内同一时刻会运行成千上万个协程。

而在其他不支持协程或者延续执行流的语言中,程序员需要非阻塞或异步编程时,就必须采用层层嵌套回调函数的CPS(Continuation-Passing Style)风格。 这种风格在逻辑复杂时,会陷入“回调地狱”(Callback Hell)的陷阱,使得代码很难读懂,维护起来很困难。

Scala语言本身并不支持协程或者延续执行流。因此,一般来说,程序员需要非阻塞或异步编程时,就必须使用类似本书第13章“外部作用和 I/O”中介绍的技术,注册回调函数或者用 for/yield 语句来执行异步操作。如果流程很复杂的话,即使是 for/yield 语法仍然会陷入回调地狱。

我对 Scala 开源社区的贡献之一是 stateless-future。这个库提供了一些宏,实现了延续执行流,可以把命令式风格的代码转换成 CPS 风格。 通过这种做法,程序员不再需要手写本书13.2节那样的代码了,编写的代码风格更像普通的 Java 或者 PHP 风格,直接像流水账一样描述顺序流程。后来,我把这种避免回调函数的思路,推广到了其他用途上。比如,我开发了基于 Scala.js 的前端框架 Binding.scala。使用 Binding.scala 的用户,编写普通的 HTML 模板,描述视图中的变量绑定关系,而不需要编写高阶函数就能做出交互复杂的网页。

而我的另一个开源库Each,则更进一步,支持一切 monad 。大多数情况下,使用了 Each 就不需要编写任何高阶函数,我称之为“没有函数的函数式编程”。 这意味,本书11章到15章的全部内容,你都可以直接编写类似 Java 的命令式语法,而 Each 则自动帮你生成使用 monad 的代码。

总之,我是 Scala 函数式编程的死对头,我写的 Scala 库,恰恰是为了避免使用本书中谆谆教导的各种高阶函数。如果你是个 Java 程序员,想在最短的时间内用 Scala 开始“搬砖”,那么,从实用角度出发,我建议你合上本书,直接用 Each 即可。因为,虽然 Each 最终会生成 Monad 风格代码,但是,本书中涉及的使用高阶函数的细节,就像汇编语言一样,就算你不知道也照样可以干活。

不过,如果你是个求道者,追求编程艺术的真理,希望刨根到底,理解函数式编程的内在理论和实现机制,那么本书很适合你。这本书绝不轻易放过每个知识点,全书包含有大量习题,要求你自己实现 Scala 标准库或者 Scalaz 中的既有功能。所以,当你读完本书,做完习题后,虽然你的应用开发能力并不会直接提升,但你会体会到构建函数式语言和框架时的难点和取舍,从而增进你的框架开发和语言设计的能力。

参考

Boost.Egg

关于 Lua 中的协程,参见A. L. de Moura, N. Rodriguez, and R. Ierusalimschy. Coroutines in Lua. Journal of Universal Computer Science, 10(7):910–925, 2004.

关于延续执行体的历史,参见Reynolds, John C. (1993). “The discoveries of continuations” (PDF). Lisp and Symbolic Computation 6 (3/4): 233–248.

关于 Scala 异步编程的“回调地狱”问题,参见Business-Friendly Functional Programming – Part 1: Asynchronous Operations

——ThoughtWorks Lead Consultant 杨博

推荐序2

函数式编程与命令式编程同样渊远流长,然而在计算机应用的历史进程中,二者的地位却颇不对等。命令式编程几乎自始至终都是大众的宠儿,函数式编程却长期局限于象牙塔和少数应用领域之内。尽管如此,函数式编程的重要性却从未被忽视,几十年来生机勃勃地发展,静静地等待着逆袭的时刻。事实上,即便是浸淫于命令式编程多年的工程师,也常常会与函数式编程亲密接触而不自知:例如 SQL、C++ 模板元编程,还有 C++ 标准库中的 STL 等,多少都带有一些函数式的色彩。早年,受软硬件水平的限制,函数式语言缺乏高效的编译器和运行时支持,这可能是函数式语言错失流行机会的一大原因。近年来,一方面程序语言理论和实现技术突飞猛进,函数式语言在性能上的劣势越来越不明显;另一方面,随着多核、高并发、分布式场景激增,大众也逐渐开始认识到函数式编程在这些领域得天独厚的优势。然而,流连于主流命令式语言多年积攒下的库、框架、社区等丰富资产,再加上长期的教育惯性与思维惯性,使得人们仍然难以在生产上完全转向函数式语言。

一个新的契机,来自于大数据。社交网络、个人移动设备、物联网等新技术的兴起,使得海量数据处理开始成为家常便饭。人们突然发现,自己在命令式世界的武器库中,竟找不出称手的兵器来攻打大数据这座堡垒。2008 年,Google 发表了跨时代的 MapReduce 论文。尽管学界对 MapReduce 颇有[非议][1],MapReduce 的核心思想仍然旋风般席卷了整个工业界,为大数据技术的发展带来了及时而深远的影响。有趣的是,MapReduce 的核心思想,正是来自于天生擅长高并发和分布式场景的函数式编程。自此以后,各色大数据处理系统层出不穷,而其中的函数式成分也愈加浓重:在用户接口层面,这些系统往往以 DSL 的形式提供一套类 SQL 语义、具函数式特征的申明式查询接口;在实现层面,则仰仗不变性等约束来简化并发和容错。然而,出于种种原因,大部分系统的实现语言仍然以 C++、Java、C# 这些命令式语言为主。可谓操命令式之形而施函数式之实。

自 2009 年起,我先后接触了 Erlang、Scheme、ML 等函数式语言。但出于显而易见的原因,未能有机会将之用于工程实战。2013 年春节前后,我参加了由 Scala 之父 Martin Odersky 在 Coursera 上开设的 Functional Programming Principles in Scala 课程。凑巧的是,就在课程结束后不久,我便得到一个机会加入 Intel 参与有关大数据和 Apache Spark 的工作。函数式语言和分布式系统一直是我的两大兴趣点,由 Scala 开发的 Spark 恰恰是二者的一个完美融合。于是,Scala 便成了我的第一门实战函数式语言。近年来 Spark 的火爆,更是对 Scala 和函数式编程的推广起到了推波助澜的作用。

与我所熟悉的其他函数式语言相比,我想 Scala 最大的优点之一就是过渡平滑。立足于 JVM 并将函数式融入如日中天的面向对象,这样的设计带来了两大明显的好处。第一,顺畅地集成了 Java 社区多年积累的财富。第二,Scala 和 C++ 类似,也是一门“广谱”多范式语言;不熟悉函数式编程的 Scala 初学者,随时可以安全地回退,转用熟悉的命令式面向对象范式编程,从而保证交付速度。这个设计的背后,应该与 Martin Odersky “学术工业两手抓、两手都很硬”的风格不无关系。论学术,他师承 Pascal 之父 Niklaus Wirth,在代码分析和程序语言设计方面建树颇丰;论工业应用,他一手打造了 Generic Java 和新一代的 javac 编译器。可以说 Martin 既具备了用以高瞻远瞩的理论基础,又十分了解普罗大众的痛点。两相结合,这才造就了 Scala 这样一个平衡于阳春白雪和下里巴人之间的作品。

其实函数式编程本身并没有多难。对于接受过若干年数学训练,却没有任何编程经验的人来说,相较于命令式编程中的破坏性赋值,函数式编程中的不变性、递归等概念反而应该更加亲切。譬如中学做证明题时,我们从不会先令 a = x,隔上几行再令 a = a + 1。真正的困难在于,函数式编程中的一些概念和手法——如用尾递归代替循环——与命令式编程的直觉相冲突。对于那些有着多年命令式语言编程经验,把 Java 用得比母语还溜的工程师们而言,一边要学习新的知识,一边还要克服多年编程训练所造成的思维定势,无异于逆水行舟。而在 Scala 中,你永远有机会在实战中回退至自己的舒适区。实际上,我们完全可以无视 Scala 的函数式特性,仅仅将 Scala 当作语法更加洗练的 Java。因此,对于那些操持主流命令式面向对象语言多年的工程师们而言,Scala 特别适合用作涉猎函数式编程的起步语言。

这本书所讲授的,正是基于 Scala 的函数式编程基础。基于 Scheme、Haskell 等老牌函数式语言的传统教材的问题在于,相关语言的语法和思维方式与读者现有的知识体系迥异,容易造成较为陡峭的入门门槛。此外,由于这些语言本身的实际应用机会不多,初学者也难以在实战中获得宝贵的直觉和经验。而在 Scala 的帮助下,这本书并不要求你抛开现有的思维方式另起炉灶,它所做的更像是为你现有的思维方式添砖加瓦,从而令你如虎添翼。

最后,作为曾经的译者,我深知在国内当前的大环境下技术翻译之不易。每一本优秀技术书籍的中译本背后都是译者数月乃至十数月的艰苦付出。感谢诸位译者为我们带来又一本好书!

——连城
2016.03.20

引用1: David J. DeWitt and Michael Stonebraker, MapReduce: A major step backwards, https://homes.cs.washington.edu/~billhowe/mapreduce_a_major_step_backwards.html