君は必ずmainのところに戻ってくる

(8/5更新)

  • 実験4を追加。

概要

Scalaでは、遅延評価が許されているので
returnがメソッドの実行ではない場所で評価されることがある。
これで遊んでみる。

実験1: foreachの中でreturn

まずは簡単な使い道。foreachに渡すブロックの中でreturnする。

package returntest

object ReturnInFor {
  def main(args : Array[String]) : Unit = {
    for(i <- List(1,2,3,4)){
      println(i + "th block call")
      val st = new Throwable().getStackTrace()
      println(st(0))
      return
    }
  }
}

実行結果

1th block call
returntest.ReturnInFor$$anonfun$main$1.apply(ReturnInFor.scala:7)

1stではないのはご愛敬。
スタックトレースを見て分かるように、
returnが評価されるときに実行されているコードは、
無名関数のapplyであり、mainではない。
しかしreturnの効果としてはmainのreturnになり、
2回目以降のブロック呼び出しはなされない。

結論

returnは、returnが含まれているメソッド呼び出しのreturnになる。

実験2: singletonオブジェクトのフィールドにmainのreturn

mainの中で、returnのみを実行するブロックを作成し、
singletonオブジェクトのフィールドに関数として格納しておく。
これでどこからでもreturnが実行できるという寸法。
これで例外を出さずに強制終了だ!
スタックトレースが嫌いな悪い子は使ってみよう!

package returntest

object GlobalReturn {
  //global return function
  var returnf : Unit => Unit = null

  def main(args : Array[String]) : Unit = {
    //set returnf in main
    returnf = (_ : Unit) => { return }

    //another thread
    val th = new Thread(){
      override def run() {
        //thread call returnf
        println("thread call return")
        returnf()
        println("thread end")
      }
    }.start()

    //returnf in method
    println("method call")
    AnotherObject.method()
    println("main end")
  }
}

object AnotherObject {
  def method() = {
    println("method start")
    try {
      println("method call return")
      GlobalReturn.returnf()      
    } finally {
      println("finally")
    }
    println("method end")
  }
}

実行結果の例(スレッドの実行順序によって多少異なる)

thread call return
Exception in thread "Thread-0" scala.runtime.NonLocalReturnException
method call
method start
method call return
finally
  • AnotherObject.methodの中でreturnf()を呼んでも、mainが終了していることがわかる。
  • finallyは実行されている。
  • mainスレッドが作った別スレッドからreturnf()を呼ぶと、scala.runtime.NonLocalReturnExceptionという面白げな例外が発生する。

Nonときたか。

実験3: 再帰呼び出しの好きなところにreturn

メソッドの再帰呼び出しがなされるたびに
returnするブロックを関数にしたものを、配列に格納しておく。
再帰呼び出しを終わらせたい時に、ランダムで1つ関数を選び、呼びだす。
さあて、どこまでreturnするのかな?

package returntest
import scala.util.Random

object RecReturn {

  def main(args : Array[String]) : Unit = {
    recCall(20)
  }
  
  def recCall(n : Int) = {
    if(n < 0){ throw new IllegalArgumentException }
    //blocks [0-n]
    rec(n, new Array[Unit => Unit](n + 1))
  }
  
  def rec(n : Int, returns : Array[Unit => Unit]) : Unit = {
    println("call with " + n)

    //set block in array
    returns(n) = (_ : Unit) => {
      println("return @ " + n)
      return 
    }

    if(n == 0){
      val rand = new Random(System.currentTimeMillis).nextInt(returns.length)
      //call random block
      returns(rand)()
    }

    //recursive call
    rec(n-1, returns)
    println("call " + n + " end")
  }
}

実行結果の例(Random.nextIntの値により異なる)

call with 20
call with 19
call with 18
call with 17
call with 16
call with 15
call with 14
call with 13
call with 12
call with 11
call with 10
call with 9
call with 8
call with 7
call with 6
call with 5
call with 4
call with 3
call with 2
call with 1
call with 0
return @ 13
call 14 end
call 15 end
call 16 end
call 17 end
call 18 end
call 19 end
call 20 end
  • 再帰呼び出しは20から0まで行われている。
  • 呼びだし13を強制returnしたらしい。
  • 14以降は普通に終了している。

かくして、どこまで戻るか分からないreturnが完成した。

(以下8/5追記)

実験4 メソッド終了後にreturn

先日は別スレッドを使ったが
リファレンスマニュアルを見ると

「returnが含まれるメソッドが終了してからreturnを呼ぶと
scala.runtime.NonLocalReturnExceptionが出る」

という趣旨のことが書いてあるので
そのパターンを試してみた。

package returntest

object AfterReturn {
  var returnf : Unit => Unit = null
  
  def main(args : Array[String]) : Unit = {
    setReturn()
    try {
      returnf()
    } catch {
      case e => e.printStackTrace(System.out)
      new Throwable().printStackTrace(System.out)
    }
    
    println("main end")
  }
  
  def setReturn() { 
    returnf = (_ : Unit) => { return }
  }
}

実行結果

scala.runtime.NonLocalReturnException
java.lang.Throwable
	at returntest.AfterReturn$.main(AfterReturn.scala:12)
	at returntest.AfterReturn.main(AfterReturn.scala)
main end

確かに例外を出せることを確認。
scala.runtime.NonLocalReturnExceptionは補足できるが
スタックトレースを作ってくれないことがわかった。