2013-12-21 11 views
9

これは通常のSO質問に反していることを認識していますが、次のコードはうまくいかないと思っても機能します。以下はwhileループで継続を使用する小さなScalaプログラムです。継承継承スタイルの私の理解によると、このコードは、whileループの繰り返しごとにフレームをスタックに追加することによってスタックオーバーフローエラーを生成するはずです。しかし、それは正常に動作します。whileループでのスカラ継続の使用

import util.continuations.{shift, reset} 


class InfiniteCounter extends Iterator[Int] { 
    var count = 0 
    var callback: Unit=>Unit = null 
    reset { 
     while (true) { 
      shift {f: (Unit=>Unit) => 
       callback = f 
      } 
      count += 1 
     } 

    } 

    def hasNext: Boolean = true 

    def next(): Int = { 
     callback() 
     count 
    } 
} 

object Experiment3 { 

    def main(args: Array[String]) { 
     val counter = new InfiniteCounter() 
     println(counter.next()) 
     println("Hello") 
     println(counter.next()) 
     for (i <- 0 until 100000000) { 
      counter.next() 
     } 
     println(counter.next()) 
    } 

} 

出力は次のようになります。

1 
Hello 
2 
100000003 

私の質問は:なぜ何のスタックオーバーフローが存在しませんか? Scalaコンパイラは、テールコールの最適化を行っていますか(継続ではできないと思っていました)、あるいは別のことが起こっていますか?

(この実験は、それを実行するために必要なSBTの設定と一緒にgithubの上にある:。https://github.com/jcrudy/scala-continuation-experimentsを7cec9befcf58820b925bb222bc25f2a48cbec4a6をコミット参照)

答えて

7

あなたがshiftを使用している方法であるため、ここでスタックオーバーフローを取得しない理由callback()は、trampolineのように動作します。

実行スレッドがshift構成に達するたびに、callbackを現在の継続(クロージャ)と等しく設定し、すぐにUnitを呼び出しコンテキストに返します。 next()を呼び出してcallback()を呼び出すと、継続クロージャが実行され、count += 1を実行した後、ループの先頭にジャンプして、shiftを再度実行します。

CPS変換の主なメリットの1つは、スタックを使用するのではなく、連続で制御の流れをキャプチャすることです。各 "反復"でcallback = fを設定すると、関数の前の継続/状態への参照のみが上書きされ、ガベージコレクションが可能になります。

ここのスタックは数フレームの深さにしか達しません(ネストされたクロージャのためにおそらく約10です)。 shiftを実行するたびに(ヒープ内の)クロージャの現在の状態が取得され、スタックはforの式に戻ります。

私は、ダイアグラムがこれをより明確にするように感じますが、デバッガでコードをステップ実行すると、おそらく同様に役立つでしょう。私はここで重要なポイントは、基本的にトランポリンを構築して以来、あなたはスタックを吹き飛ばすことは決してないだろうと思う。

+0

私はそれがかなり混乱しているものの大きな説明だと思います。私はこれを視覚的にスケッチしようとするかもしれない。これを明確にするために、この構成はスタックを吹き飛ばすだけでなく、全体のメモリ要件も控えめであり、「反復回数」に依存しないことを意味します。 – jcrudy

+2

@jcrudy - はい、メモリ要件は 'for'式の繰り返し回数に依存しません。簡単なテストとして、私はあなたのコードを次のように実行しました: 'JAVA_OPTS =" - Xmx2M -verbose:gc "scala Experiment3'(私のラップトップで動作します)これは、100Mの反復がちょうど2MBのヒープ空間で動作することを証明しています。冗長ガベージコレクションのオプションを追加することで、実際のメモリ使用量が実行中ほぼ一定に保たれることがわかります。 – DaoWen

+0

ScalaのトランポリンにRich Doughertyが投稿した素敵なイラストのブログ記事があります:http://blog.richdougherty.com/2009/04/tail-calls-tailrec-and-trampolines.html – jcrudy

関連する問題