Garland +

Scala 匿名函数中的 return

Q

之前写 Scala 代码在 foreach 上和领导 argue 了一波,原因挺有意思,作文以记之。

大概是这样一段代码

def flatMap(
    events: Set[String], 
    t: String, 
    collector: Collector[String]
 ): Unit = {
    events
    .foreach(f => {
      if (t.contains(f)) {
        collector.collect(t)
        return
      }
    })
}

events: Set[String] 中存放了一些关键字,然后要判断一下传入的日志 t 是否包含某个关键字,包含的话就把日志 t 发出去,因为这里能保证一条 日志 t 不会同时包含多个关键字,所以为了省一些比较时间这里一旦命中就直接 return 了(当然第一版我用了一个 hasEvent: Boolean 标志位被领导说太费解了所以才换成了 return 2333333),于是产生了这么一波对话

Leader: 是不是写俩 for 循环跟直观一点
我: 为啥要俩 for 啊,events 过一遍找到一个满足条件的就直接发出去了吧
Leader: 外层的 foreach 然后里面有 return,脑子不太绕的过来 return 是return到什么流程里面。好像现在的写法,return 结束的是当前的 foreach 循环,并不会结束 flatMap. 
我: scala 里面 for 和 foreach 没差吧,而且 return 这个关键字的语义不是很确定么,就是返回函数值,没哪个语言的 return 在循环内使用是跳出循环的意思吧。

说完最后一句话,心里有点凉,毕竟没啥底气就赶紧查了下,领导的意思是 foreach 的参数是个匿名函数,如下

def foreach[U](f: A => U): Unit =
  iterator.foreach(f)

匿名函数里的 return 的作用域应该是这个匿名函数内部,所以上面那段代码里的 return 并不能起到 early return 的作用。

道理是这样,而且我好像无法反驳,然而我写了个 demo 发现并不是这样,是能起到 early return 的作用的,如下

object ReturnExample {
  def main(args: Array[String]): Unit = {
    assert(run() == "orange")
  }

  def run(): String = {
    val events = Set("apple", "orange", "grape")
    events.foreach(f => {
      if (f == "orange") {
        return f
      }
    })
    "monkey!!!!!!!"
  }
}

所以细化一下就是两个问题了

  1. Scala foreachfor 的区别是什么?
  2. 匿名函数里的 return 到底 return 到哪儿了?

Why

foreach && for

先说结论[1]

  1. 简单 for 一个 collection 会在底层转为 collection 上的 foreach 方法调用
  2. 带 if for 一个 collection 会在底层转为 collection.withFilter.foreach 方法调用
  3. 带 yield for 一个 collection 会在底层转为 collection 上的 map 方法调用
  4. 带 if 和 yield for 一个 collection 会在底层转为 collection.withFilter.map 方法调用

这里看一下底层是如何转化的,比如有这么一段代码

class Main {
    for (i <- 1 to 10) println(i)
}

使用 scalac -Xprint:parse Main.scala 可得

$ scalac -Xprint:parse Main.scala
[[syntax trees at end of parser]] // Main.scala
package <empty> {
  class Main extends scala.AnyRef {
    def <init>() = {
      super.<init>();
      ()
    };
    1.to(10).foreach(((i) => println(i)))
  }
}

可以看到 for 被转成了 foreach 方法调用,其他的比如带 ifyield 的可以自行尝试

匿名函数中的 return

先说结论[2]

return 返回到的是最近的带名方法/函数

最近的带名方法/函数 f 有显示声明的返回值类型,return 表达式进行求值得到值 e,然后把这个值返回给 f, 对上面的 demo 代码编译看看:

→ scalac -Xprint:explicitouter ReturnExample.scala
[[syntax trees at end of             explicitouter]] // ReturnExample.scala
package com.xiachufang.spark.job.tarPlatform {
  object ReturnExample extends Object {
    def <init>(): com.xiachufang.spark.job.tarPlatform.ReturnExample.type = {
      ReturnExample.super.<init>();
      ()
    };
    def main(args: Array[String]): Unit = scala.Predef.assert(ReturnExample.this.run().==("orange"));
    def run(): String = {
      <synthetic> val nonLocalReturnKey1: Object = new Object();
      try {
        val events: scala.collection.immutable.Set[String] = scala.Predef.Set().apply[String](scala.Predef.wrapRefArray[String](Array[String]{"apple", "orange", "grape"}));
        events.foreach[Unit]({
          final <artifact> def $anonfun$run(f: String): Unit = if (f.==("orange"))
            throw new scala.runtime.NonLocalReturnControl[String](nonLocalReturnKey1, f)
          else
            ();
          ((f: String) => $anonfun$run(f))
        });
        "monkey!!!!!!!"
      } catch {
        case (ex @ (_: scala.runtime.NonLocalReturnControl[String @unchecked])) => if (ex.key().eq(nonLocalReturnKey1))
          ex.value()
        else
          throw ex
      }
    }
  }
}

可以看到,return 那里被换成了抛出一个 NonLocalReturnControl 异常,然后整个函数顶层有一个大的 try-catch 来捕获这个异常,拿到里面的 value 返回,来靠这种方式实现 early-return

看看别的语言

python: 匿名函数里不支持 return 关键字

java 也是不支持在匿名函数里 return 的,IDE 会报警

private int test() {
    List<Integer> array = Arrays.asList(1, 2, 3);
    array.forEach(num -> {
        return num;
    });
    return 100000;
}

不过这种写法确实很 confusing,尤其是不同语言之间还有区别,这时候就靠 IDE 和编译器来保证了;私以为这可能也是某种 Scala 不推荐 用 return 关键字的原因吧,比如用标志位来实现

→ scalac -Xprint:explicitouter ReturnExample.scala
[[syntax trees at end of             explicitouter]] // ReturnExample.scala
package com.xiachufang.spark.job.tarPlatform {
  object ReturnExample extends Object {
    def <init>(): com.xiachufang.spark.job.tarPlatform.ReturnExample.type = {
      ReturnExample.super.<init>();
      ()
    };
    def main(args: Array[String]): Unit = scala.Predef.assert(ReturnExample.this.run().==("orange"));
    def run(): String = {
      val events: scala.collection.immutable.Set[String] = scala.Predef.Set().apply[String](scala.Predef.wrapRefArray[String](Array[String]{"apple", "orange", "grape"}));
      var hasEvent: Boolean = false;
      var res: String = "";
      events.foreach[Unit]({
        final <artifact> def $anonfun$run(f: String): Unit = if (hasEvent.unary_!().&&(f.==("orange")))
          {
            res = f;
            hasEvent = true
          }
        else
          ();
        ((f: String) => $anonfun$run(f))
      });
      res
    }
  }
}

就感觉优雅了很多,虽然都是同样的结果,但是从逻辑上来看,没有 early return,也就没有副作用了吧。。。。。。

REF:

  1. Scala: How to loop over a collection with ‘for’ and ‘foreach’ (plus for loop translation)
  2. Scala return statements in anonymous functions
言:

Blog

Thoughts

Project