scala雾中风景(1): lambda表达式的缩写

scala中的函数是可以像普通变量一样来传递的,也允许方法的参数是函数类型。运行时传递一个函数变量,或者用lambda表达式描述的一段匿名函数。比如:

// 普通函数
scala> def foo(s:String) = { println(s) }

// 高阶函数
scala> def hf(f:String=>Unit) = f("higher")

// 把普通函数赋值给变量f
scala> val f:String=>Unit = foo

// 上面的函数类型String=>Unit 也可以写为Function1[String,Unit]
scala>  val f:Function1[String,Unit] = foo

// 把变量 f 当作参数传递给高级函数hf
scala> hf(f)
higher

现在看看以lambda的形式

scala> hf((s:String)=>println(s))
higher

这里使用lambda表达式 (s:String) => println(s) 作为匿名函数传入的,有趣的是,它可以简化:

scala> hf(s=>println(s)) // 第1个简化版本
higher

scala> hf(println(_)) // 第2个简化版本
higher

scala> hf(println _) // 第3个简化版本
higher

scala> hf(println) // 第4个简化版本
higher

上面的简化版本里前面的都好理解。第1个版本省略了s的类型,这是因为在定义hf时已经声明了参数类型为String=>Unit,编译器会把lambda表达式中的入参和出参按声明的类型来对待。

第2个版本则是采用了占位符的方式。它的形式为,对于所有的 x=>g(x) 都可以用占位符的形式写为:g(_),相当于省去了lambda表达式的入参和箭头部分,然后用占位符表示入参。

第3个简化版本是第2个版本的变种,是函数式语言里的一个习俗,scala继承了这一风格:当只有一个参数时,函数的小括号可写可不写。f(x) 可以写为 f x

//2014.9月更正,上面中划线部分是错误的说法,单独调用函数时小括号部分不可省略,单个参数的函数其小括号只有在 Object func param这种形式下可省略,比如 a equals b

第3个版本和第2个版本都是部分应用函数(partial applied function)的写法,当只有一个参数时,这两个写法是等价的。

让人困惑的是第4个版本!为什么连参数都可以省略?这还是一个lambda表达式么?是所有的lambda都可以缩写成这种形式么?

第4个版本的背后其实是编译器支持lambda的“eta转换”(可以参考之前的文章scala中的eta-conversion)。
简单的说就是对于lambda表达式中只有一个参数,并且箭头右边的逻辑是对入参执行一个函数:即 x => f(x)
则可以简写为f

所以第4个版本的简化就是eta转换,根据上面的说法,并不是所有的lambda表达式都可以缩写成这个样子的。

小结:lambda表达式的简化有时让人觉得云里雾里,但每个简化形式背后都有规则或习俗。eta-conversion是最容易迷惑的一种形式,它让表达式看上去不像是lambda表达式。

scala雾中风景(1): lambda表达式的缩写》上有6条评论

  1. leezq

    博主,你好:
    博文很不错,受益匪浅,关于第三种简化方式没有看懂。能否解释下
    f(x)可以写成f x
    那println(“Hello”) 怎么不能写成println “Hello”呢
    而且为什么不可以这么写呢:hf(s => {println s})

    回复
    1. hongjiang 文章作者

      那部分写错了,已经更正。
      println(_) 和 println _ 都属于部分应用函数(partial applied function) 会被编译器通过eta扩展转换为
      (x:Any) => println(x) 形式的函数对象
      这里并不是单个参数方法小括号可以省略的情况。对于只有一个参数的方法,其部分应用函数写法 println(_) 和 println _ 在效果上是等价的。

      再说单个参数的方法小括号可以省略的情况,println 方法是 Predef 里定义的一个快捷方式,要写为:
      scala> Predef println “ok”
      ok
      或:
      scala> Console println “ok”
      ok
      也就是说方法前边的对象不能省略,即只能对 obj.foo(param) 这种形式写为 obj foo param。

      scala> object I { def say(s:String) = println(s) }
      defined object I

      scala> I.say(“hi”)
      hi
      可省略对象与方法中间的点

      scala> I say(“hi”)
      hi

      此时针对单个参数的方法才可进一步的省略小括号
      scala> I say “hi”
      hi

      回复
      1. leezq

        感谢博主细致的解答!
        最近在学scala,算是scala的新人。非常喜欢这个系列。博主加油!

        回复
  2. xlab

    第四个可不可以直接看做是函数调用,不涉及 lambda 表达式?hf 接受 String => Unit 类型的输入参数,而 println 恰好是这种类型的,可以直接用作输入参数。没看 Predef 源码,乱说的。

    回复
    1. hongjiang 文章作者

      我猜测你想说的是将 println 方法,转换为一个函数对象,因为 println 的类型是 Any=>Unit 符合 String => Unit 的类型要求,所以不必再进行一次 lambda 转换 s => println(s) ;不过除非你显式的柯里化( println _ 这种方式),否则编译器一定会做 eta expansion,参考 http://hongjiang.info/eta-conversion-and-eta-expansion/

      回复
  3. yychaoV5

    第三中简化 可以理解为Eta-expansion ,只是println只有一个参数 ?

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注