Garland +

Scala 匿名函数中的 return

Q

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

大概是这样一段代码

  1. def flatMap(
  2. events: Set[String],
  3. t: String,
  4. collector: Collector[String]
  5. ): Unit = {
  6. events
  7. .foreach(f => {
  8. if (t.contains(f)) {
  9. collector.collect(t)
  10. return
  11. }
  12. })
  13. }

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

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

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

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

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

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

  1. object ReturnExample {
  2. def main(args: Array[String]): Unit = {
  3. assert(run() == "orange")
  4. }
  5. def run(): String = {
  6. val events = Set("apple", "orange", "grape")
  7. events.foreach(f => {
  8. if (f == "orange") {
  9. return f
  10. }
  11. })
  12. "monkey!!!!!!!"
  13. }
  14. }

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

  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 方法调用

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

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

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

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

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

匿名函数中的 return

先说结论[2]

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

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

  1. scalac -Xprint:explicitouter ReturnExample.scala
  2. [[syntax trees at end of explicitouter]] // ReturnExample.scala
  3. package com.xiachufang.spark.job.tarPlatform {
  4. object ReturnExample extends Object {
  5. def <init>(): com.xiachufang.spark.job.tarPlatform.ReturnExample.type = {
  6. ReturnExample.super.<init>();
  7. ()
  8. };
  9. def main(args: Array[String]): Unit = scala.Predef.assert(ReturnExample.this.run().==("orange"));
  10. def run(): String = {
  11. <synthetic> val nonLocalReturnKey1: Object = new Object();
  12. try {
  13. val events: scala.collection.immutable.Set[String] = scala.Predef.Set().apply[String](scala.Predef.wrapRefArray[String](Array[String]{"apple", "orange", "grape"}));
  14. events.foreach[Unit]({
  15. final <artifact> def $anonfun$run(f: String): Unit = if (f.==("orange"))
  16. throw new scala.runtime.NonLocalReturnControl[String](nonLocalReturnKey1, f)
  17. else
  18. ();
  19. ((f: String) => $anonfun$run(f))
  20. });
  21. "monkey!!!!!!!"
  22. } catch {
  23. case (ex @ (_: scala.runtime.NonLocalReturnControl[String @unchecked])) => if (ex.key().eq(nonLocalReturnKey1))
  24. ex.value()
  25. else
  26. throw ex
  27. }
  28. }
  29. }
  30. }

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

看看别的语言

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

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

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

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

  1. scalac -Xprint:explicitouter ReturnExample.scala
  2. [[syntax trees at end of explicitouter]] // ReturnExample.scala
  3. package com.xiachufang.spark.job.tarPlatform {
  4. object ReturnExample extends Object {
  5. def <init>(): com.xiachufang.spark.job.tarPlatform.ReturnExample.type = {
  6. ReturnExample.super.<init>();
  7. ()
  8. };
  9. def main(args: Array[String]): Unit = scala.Predef.assert(ReturnExample.this.run().==("orange"));
  10. def run(): String = {
  11. val events: scala.collection.immutable.Set[String] = scala.Predef.Set().apply[String](scala.Predef.wrapRefArray[String](Array[String]{"apple", "orange", "grape"}));
  12. var hasEvent: Boolean = false;
  13. var res: String = "";
  14. events.foreach[Unit]({
  15. final <artifact> def $anonfun$run(f: String): Unit = if (hasEvent.unary_!().&&(f.==("orange")))
  16. {
  17. res = f;
  18. hasEvent = true
  19. }
  20. else
  21. ();
  22. ((f: String) => $anonfun$run(f))
  23. });
  24. res
  25. }
  26. }
  27. }

就感觉优雅了很多,虽然都是同样的结果,但是从逻辑上来看,没有 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