Scalaの継続とは何ですか?なぜそれらを使用するのですか?


85

Scalaでのプログラミングを終えたばかりで、Scala2.7と2.8の間の変更点を調べています。最も重要と思われるのは継続プラグインですが、それが何に役立つのか、どのように機能するのかわかりません。非同期I / Oに適していることは確認しましたが、その理由を見つけることができませんでした。このテーマに関するより人気のあるリソースのいくつかは次のとおりです。

そして、Stack Overflowに関するこの質問:

残念ながら、これらのリファレンスはいずれも、継続の目的やシフト/リセット関数の機能を定義しようとはしておらず、そのようなリファレンスは見つかりませんでした。リンクされた記事の例がどのように機能するか(またはそれらが何をするか)を推測することができなかったので、私を助ける1つの方法は、それらのサンプルの1つを1行ずつ調べることです。3番目の記事からのこの単純なものでさえ:

reset {
    ...
    shift { k: (Int=>Int) =>  // The continuation k will be the '_ + 1' below.
        k(7)
    } + 1
}
// Result: 8

結果が8なのはなぜですか?それはおそらく私が始めるのに役立つでしょう。


回答:


38

私のブログは何を何resetをするかを説明しているshiftので、もう一度読んでみてください。

私もブログで指摘しているもう1つの優れた情報源は、継続渡しスタイルに関するWikipediaのエントリです。これは、Scala構文を使用せず、継続が明示的に渡されますが、これまでで最も明確です。

ブログでリンクしているが壊れているように見える、区切られた継続に関する論文には、多くの使用例が示されています。

しかし、区切られた継続の概念の最良の例はScalaSwarmだと思います。その中で、ライブラリは停止しますある時点でコードの実行をし、残りの計算は継続になります。次に、ライブラリは何かを実行します。この場合、計算を別のホストに転送し、結果(アクセスされた変数の値)を停止された計算に返します。

さて、あなたはそう、Scalaのページでも、簡単な例を理解していない私のブログを読んでください。その中で私はこれらの基本を説明することだけに関心があり、結果がなぜであるかについてです8


私はあなたのブログエントリを読み直しました、そして今度はそれに固執しました-私は何が起こっているかについてより良い考えを持っていると思います。私はウィキペディアのページから多くを得ることができませんでした(私はすでにLispの継続を知っています)が、リセット/シフトの延期されたスタイルまたはそれが呼ばれるものは何でも私を困惑させました。せっかちな人(つまり私自身)にとって、あなたの説明は大丈夫でしたが、人々は「リセットの結果はシフト内のコードの結果です」までそれを守る必要があります。段落...私はその時点まで絶望的に失われましたが、それはより明確になります!これが何のためにあるのかまだ興味があるので、Swarmを見ていきます。どうも!
デイブ

はい、物事が意味をなし始めるまでには時間がかかります。説明を早くすることができるとは思いませんでした。
ダニエルC.ソブラル

私はリセットが継続の範囲を区切る」という認識に来たとき、それはすべての私のために一緒に来た(すなわち:変数と記述が含まれる。)。
JeffV

1
あなたの説明は冗長であり、理解の本質に到達しませんでした。例は長く、最初の段落ではすべてを読むように促すのに十分な理解が得られませんでした。だから私はそれを投票しました。SOは、投票後にコメントを追加するように求めるメッセージを表示するので、準拠しています。率直に申し訳ありません。
シェルビームーアIII

1
私はこれについて、制御フローの理解に焦点を当ててブログに書いています(実装の詳細については説明しません)。wherenullpoints.com/2014/04/scala-continuations.html
Alexandros

31

既存の説明は、私が望むよりも概念を説明するのに効果的ではないことがわかりました。これが明確(かつ正しい)であることを願っています。私はまだ継続を使用していません。

継続関数cfが呼び出されたとき:

  1. 実行はshiftブロックの残りをスキップし、ブロックの最後から再開します
    • 渡されるパラメータは、実行が続行さcfれるときにshiftブロックが「評価」するものです。これは、への呼び出しごとに異なる可能性がありますcf
  2. 実行は、resetブロックが終了するまで(または、ブロックresetがない場合はへの呼び出しまで)続行されます。
    • resetブロックの結果(またはresetブロックがない場合は()へのパラメーター)がcf返されます
  3. その後cfshiftブロックが終了するまで実行が継続されます
  4. resetブロックの終わり(またはリセットの呼び出し?)まで実行はスキップします

したがって、この例では、AからZまでの文字に従ってください

reset {
  // A
  shift { cf: (Int=>Int) =>
    // B
    val eleven = cf(10)
    // E
    println(eleven)
    val oneHundredOne = cf(100)
    // H
    println(oneHundredOne)
    oneHundredOne
  }
  // C execution continues here with the 10 as the context
  // F execution continues here with 100
  + 1
  // D 10.+(1) has been executed - 11 is returned from cf which gets assigned to eleven
  // G 100.+(1) has been executed and 101 is returned and assigned to oneHundredOne
}
// I

これは印刷します:

11
101

2
コンパイルしようとすると、「CPS変換された関数の結果の型を計算できません」というエラーが表示されます。修正方法もわかりません
Fabio Veronez 2011年

@Fabio Veronezシフトの最後にreturnステートメントを追加します。println(oneHundredOne) }たとえば、に変更しprintln(oneHundredOne); oneHundredOne }ます。
folone 2011年

恐ろしい構文の良い説明。継続関数の宣言は、奇妙なことにその本体から切り離されています。私はそのような頭を悩ませるコードを他の人と共有するのは気が進まないでしょう。
joeytwiddle 2012年

cannot compute type for CPS-transformed function resultエラーを回避するために、の+1直後に続くものとしますoneHundredOne}。現在それらの間にあるコメントは、どういうわけか文法を破ります。
lcn 2015年

9

Scalaの区切られた継続に関する研究論文の標準的な例を考えると、への関数入力にshift名前が付けられ、f匿名ではなくなるように少し変更されています。

def f(k: Int => Int): Int = k(k(k(7)))
reset(
  shift(f) + 1   // replace from here down with `f(k)` and move to `k`
) * 2

Scalaのプラグイン変換この例ように(の入力引数内演算resetそれぞれから始まる)shiftの呼び出しに resetされる交換機能(例えば、とfする)入力shift

置き換えられた計算は、関数にシフト(つまり移動)されますk。機能はf機能入力しkk 含まれている置き換え計算、k入力x: Int、およびで計算k置き換えshift(f)としますx

f(k) * 2
def k(x: Int): Int = x + 1

これは次と同じ効果があります:

k(k(k(7))) * 2
def k(x: Int): Int = x + 1

Int入力パラメーターxのタイプ(つまり、のタイプ署名k)は、の入力パラメーターのタイプ署名によって指定されていることに注意してくださいf

概念的に同等の抽象化を使用した別の借用例、つまり:readへの関数入力shiftです。

def read(callback: Byte => Unit): Unit = myCallback = callback
reset {
  val byte = "byte"

  val byte1 = shift(read)   // replace from here with `read(callback)` and move to `callback`
  println(byte + "1 = " + byte1)
  val byte2 = shift(read)   // replace from here with `read(callback)` and move to `callback`
  println(byte + "2 = " + byte2)
}

これは、次の論理的等価物に変換されると思います。

val byte = "byte"

read(callback)
def callback(x: Byte): Unit {
  val byte1 = x
  println(byte + "1 = " + byte1)
  read(callback2)
  def callback2(x: Byte): Unit {
    val byte2 = x
    println(byte + "2 = " + byte1)
  }
}

これにより、これら2つの例の事前の提示によっていくらか難読化された一貫性のある共通の抽象化が解明されることを願っています。たとえば、正規の最初の例は、私の名前ではなく無名関数として研究論文に提示されたfため、一部の読者にreadは、借用した2番目の例のに抽象的に類似していることがすぐにはわかりませんでした。

このように区切られた継続は、「あなたは私を外側から呼ぶreset」から「私はあなたを内側に呼ぶ」への制御の反転の幻想を生み出しますreset

戻り値の型に注意fですが、しかしk、の戻り値の型と同じである必要はないreset、すなわちfのための任意の戻り値の型を宣言する自由を持っているk限りとf同じ型を返しますreset。同上用readcapture(も参照ENV下)。


区切られた継続は、状態の制御を暗黙的に反転させるものreadcallbackはありません。たとえば、純粋関数ではありません。したがって、呼び出し元は参照透過性の式を作成できず、したがって、意図された命令セマンティクスに対する宣言型(別名透過性)の制御がありません。

継続が区切られた純粋関数を明示的に実現できます。

def aread(env: ENV): Tuple2[Byte,ENV] {
  def read(callback: Tuple2[Byte,ENV] => ENV): ENV = env.myCallback(callback)
  shift(read)
}
def pure(val env: ENV): ENV {
  reset {
    val (byte1, env) = aread(env)
    val env = env.println("byte1 = " + byte1)
    val (byte2, env) = aread(env)
    val env = env.println("byte2 = " + byte2)
  }
}

これは、次の論理的等価物に変換されると思います。

def read(callback: Tuple2[Byte,ENV] => ENV, env: ENV): ENV =
  env.myCallback(callback)
def pure(val env: ENV): ENV {
  read(callback,env)
  def callback(x: Tuple2[Byte,ENV]): ENV {
    val (byte1, env) = x
    val env = env.println("byte1 = " + byte1)
    read(callback2,env)
    def callback2(x: Tuple2[Byte,ENV]): ENV {
      val (byte2, env) = x
      val env = env.println("byte2 = " + byte2)
    }
  }
}

明示的な環境のため、これはうるさくなっています。

正直なところ、ScalaにはHaskellのグローバル型推論がないunitため、Haskellのグローバル(Hindley-Milner)型推論のため、状態モナドへの暗黙的なリフティングをサポートできませんでした(明示的な環境を隠すための1つの可能な戦略として)。ダイヤモンドの複数の仮想継承をサポートしていないことに依存します。


私は提案していますことをreset/shiftに変更することがdelimit/ replace。そして慣例により、ことfreadなりwith、かつkcallbackなりreplacedcapturedcontinuation、またはcallback
シェルビームーアIII

withはキーワードです。PSいくつかのリセットには()があります。これは{}とにかく素晴らしい記事です!
nafg 2012年

@nafgありがとうございますので、replacement代わりに提案させていただきますwith。アファイク、()許可されていますか?Afaik{}「クロージャのためのScalaの軽量構文」であり、基礎となる関数呼び出しを隠しています。たとえば、私がダニエルsequenceをどのように書き直した見てください(コードはコンパイルもテストもされていないので、遠慮なく訂正してください)。
シェルビームーアIII

1
ブロック(つまり、複数のステートメントを含む式)には中括弧が必要です。
nafg 2012年

@nafg、正解です。Afaikshift resetはライブラリ関数であり、キーワードではありません。したがって{}またはは()関数が1つのパラメーターのみを予期する場合に使用できます。Scalaは、パラメータのタイプがある場合に名前によるパラメータ(スカラでのプログラミング、第2版。PGのセクション「9.5コントロール抽象化」を参照。218)を有し、除去することができます。ブロックが呼び出される前に評価する必要があるため、名前ではなく仮定しますが、複数のステートメントが必要です。明らかに関数型を入力するので、私の使用法は正しいです。() => ...() =>Unitreset{}shift
シェルビームーアIII

8

継続は、後で呼び出されるように、計算の状態をキャプチャします。

シフト式を終了してからリセット式を関数として終了するまでの計算について考えてみてください。シフト式の中では、この関数はkと呼ばれ、継続です。あなたはそれを回して、後でそれを呼び出すことができます。

リセット式によって返される値は、=>の後のシフト式内の式の値だと思いますが、これについてはよくわかりません。

したがって、継続を使用すると、かなり恣意的で非ローカルなコードを関数にまとめることができます。これは、コーリングやバックトラッキングなどの非標準の制御フローを実装するために使用できます。

したがって、継続はシステムレベルで使用する必要があります。アプリケーションコードにそれらを振りかけることは、悪夢の確実なレシピであり、gotoを使用した最悪のスパゲッティコードよりもはるかに悪いものです。

免責事項:私はScalaの継続について深く理解していません。例を見て、Schemeの継続を​​知っていることから推測しました。


5

私の観点から、最良の説明はここに与えられました:http//jim-mcbeath.blogspot.ru/2010/08/delimited-continuations.html

例の1つ:

制御フローをもう少し明確に確認するには、次のコードスニペットを実行します。

reset {
    println("A")
    shift { k1: (Unit=>Unit) =>
        println("B")
        k1()
        println("C")
    }
    println("D")
    shift { k2: (Unit=>Unit) =>
        println("E")
        k2()
        println("F")
    }
    println("G")
}

上記のコードが生成する出力は次のとおりです。

A
B
D
E
G
F
C

1

別の(より最近の- 2016年5月)Scalaの継続についての記事です:
" Scalaではタイムトラベル:スカラ座(Scalaの継続)におけるCPS "によって Shivansh Srivastava氏(shiv4nsh
また、を指しジムMcBeath記事で述べたドミトリーBespalov s 'の答え

しかしその前に、それは次のような継続について説明しています:

続きは、コンピュータプログラムの制御状態の抽象的な表現です。
つまり、実際に意味するのは、プロセスの実行の特定の時点での計算プロセスを表すデータ構造であるということです。作成されたデータ構造は、ランタイム環境に隠される代わりに、プログラミング言語からアクセスできます。

さらに説明すると、最も古典的な例の1つがあります。

冷蔵庫の前のキッチンにいて、サンドイッチのことを考えているとしましょう。あなたはすぐそこに続きを取り、あなたのポケットにそれを貼り付けます。
次に、冷蔵庫から七面鳥とパンを取り出し、サンドイッチを作ります。サンドイッチは現在、カウンターに座っています。
ポケットの中で継続を呼び出すと、サンドイッチのことを考えて、再び冷蔵庫の前に立っていることに気づきます。しかし、幸いなことに、カウンターにはサンドイッチがあり、それを作るために使用されていたすべての材料がなくなっています。だからあなたはそれを食べます。:-)

この説明では、sandwichプログラムデータの一部(ヒープ上のオブジェクトなど)であり、「make sandwich」ルーチンを呼び出して戻るのではなく、「」ルーチンを呼び出しmake sandwich with current continuationてサンドイッチを作成し、実行を続行します。やめた。

そうは言っても、Scala2.11.0-RC1について2014年4月に発表されたように

次のモジュールを引き継ぐメンテナを探しています:scala-swingscala-continuations
新しいメンテナが見つからない場合、2.12にはそれらが含まれません
他のモジュール(scala-xml、scala-parser-combinators)を維持し続ける可能性がありますが、それでも助けていただければ幸いです。


0

意味のある例によるScalaの継続

from0to100から10までの反復のアイデアを表すを定義しましょう。

def from0to10() = shift { (cont: Int => Unit) =>
   for ( i <- 0 to 10 ) {
     cont(i)
   }
}

さて、

reset {
  val x = from0to10()
  print(s"$x ")
}
println()

プリント:

0 1 2 3 4 5 6 7 8 9 10 

実際、私たちは必要ありませんx

reset {
  print(s"${from0to10()} ")
}
println()

同じ結果を出力します。

そして

reset {
  print(s"(${from0to10()},${from0to10()}) ")
}
println()

すべてのペアを出力します:

(0,0) (0,1) (0,2) (0,3) (0,4) (0,5) (0,6) (0,7) (0,8) (0,9) (0,10) (1,0) (1,1) (1,2) (1,3) (1,4) (1,5) (1,6) (1,7) (1,8) (1,9) (1,10) (2,0) (2,1) (2,2) (2,3) (2,4) (2,5) (2,6) (2,7) (2,8) (2,9) (2,10) (3,0) (3,1) (3,2) (3,3) (3,4) (3,5) (3,6) (3,7) (3,8) (3,9) (3,10) (4,0) (4,1) (4,2) (4,3) (4,4) (4,5) (4,6) (4,7) (4,8) (4,9) (4,10) (5,0) (5,1) (5,2) (5,3) (5,4) (5,5) (5,6) (5,7) (5,8) (5,9) (5,10) (6,0) (6,1) (6,2) (6,3) (6,4) (6,5) (6,6) (6,7) (6,8) (6,9) (6,10) (7,0) (7,1) (7,2) (7,3) (7,4) (7,5) (7,6) (7,7) (7,8) (7,9) (7,10) (8,0) (8,1) (8,2) (8,3) (8,4) (8,5) (8,6) (8,7) (8,8) (8,9) (8,10) (9,0) (9,1) (9,2) (9,3) (9,4) (9,5) (9,6) (9,7) (9,8) (9,9) (9,10) (10,0) (10,1) (10,2) (10,3) (10,4) (10,5) (10,6) (10,7) (10,8) (10,9) (10,10) 

さて、それはどのように機能しますか?

あると呼ばれるコードはfrom0to10、および呼び出しコード。この場合、次のブロックですreset。呼び出されたコードに渡されるパラメーターの1つは、呼び出し元のコードのどの部分がまだ実行されていないかを示す戻りアドレスです(**)。呼び出しコードのその部分は継続です。呼び出されたコードは、そのパラメーターを使用して、制御を渡すか、無視するか、複数回呼び出すかを決定できます。ここでfrom0to10は、0..10の範囲の各整数の継続を呼び出します。

def from0to10() = shift { (cont: Int => Unit) =>
   for ( i <- 0 to 10 ) {
     cont(i) // call the continuation
   }
}

しかし、継続はどこで終わりますか?return継続からの最後のコードが呼び出されたコードに制御を返すため、これは重要from0to10です。Scalaでは、resetブロックが終了する場所で終了します(*)。

これで、継続がとして宣言されていることがわかりcont: Int => Unitます。どうして?from0to10として呼び出しval x = from0to10()Intはに移動する値のタイプですxUnit後のブロックresetは値を返さないようにする必要があることを意味します(そうしないと、タイプエラーが発生します)。一般に、関数入力、継続入力、継続結果、関数結果の4つのタイプのシグニチャがあります。4つすべてが呼び出しコンテキストと一致する必要があります。

上記では、値のペアを出力しました。九九を印刷してみましょう。しかし\n、各行の後にどのように出力するのでしょうか?

この関数をback使用すると、継続からそれを呼び出したコードまで、制御が戻ったときに何を実行する必要があるかを指定できます。

def back(action: => Unit) = shift { (cont: Unit => Unit) =>
  cont()
  action
}

back最初にその継続を呼び出し、次にアクションを実行します

reset {
  val i = from0to10()
  back { println() }
  val j = from0to10
  print(f"${i*j}%4d ") // printf-like formatted i*j
}

印刷します:

   0    0    0    0    0    0    0    0    0    0    0 
   0    1    2    3    4    5    6    7    8    9   10 
   0    2    4    6    8   10   12   14   16   18   20 
   0    3    6    9   12   15   18   21   24   27   30 
   0    4    8   12   16   20   24   28   32   36   40 
   0    5   10   15   20   25   30   35   40   45   50 
   0    6   12   18   24   30   36   42   48   54   60 
   0    7   14   21   28   35   42   49   56   63   70 
   0    8   16   24   32   40   48   56   64   72   80 
   0    9   18   27   36   45   54   63   72   81   90 
   0   10   20   30   40   50   60   70   80   90  100 

さて、今はいくつかの脳のねじれの時間です。の呼び出しは2つありますfrom0to10。最初の続きは何from0to10ですか?バイナリコードfrom0to10での呼び出しに続きますが、ソースコードでは代入ステートメントも含まれています。ブロックが終了するところで終了しますが、ブロックの終了は制御を最初のに戻しません。ブロックの終わりは制御を2番目に戻し、2番目は最終的に制御をに戻し、それは制御をの最初の呼び出しに戻します。最初の(yes!1st!)が終了すると、ブロック全体が終了します。val i =resetresetfrom0to10resetfrom0to10backbackfrom0to10from0to10reset

制御を戻すこのような方法はバックトラッキングと呼ばれ、少なくともPrologおよびAI指向のLisp派生物の時代から知られている非常に古い手法です。

名前resetshiftは誤称です。これらの名前は、ビット演算用に残しておく必要があります。reset継続境界を定義shiftし、呼び出しスタックから継続を取得します。

ノート)

(*)Scalaでは、継続はresetブロックが終了するところで終了します。別の可能なアプローチは、関数が終了する場所で終了させることです。

(**)呼び出されたコードのパラメーターの1つは、呼び出し元のコードのどの部分がまだ実行されていないかを示す戻りアドレスです。そうですね、Scalaでは、一連のリターンアドレスがそのために使用されます。幾つ?resetブロックに入ってからコールスタックに配置されたすべてのリターンアドレス。


UPDパート2 継続の破棄:フィルタリング

def onEven(x:Int) = shift { (cont: Unit => Unit) =>
  if ((x&1)==0) {
    cont() // call continuation only for even numbers
  }
}
reset {
  back { println() }
  val x = from0to10()
  onEven(x)
  print(s"$x ")
}

これは印刷します:

0 2 4 6 8 10 

2つの重要な操作を除外しましょう:継続を破棄する(fail())とそれに制御を渡す(succ()):

// fail: just discard the continuation, force control to return back
def fail() = shift { (cont: Unit => Unit) => }
// succ: does nothing (well, passes control to the continuation), but has a funny signature
def succ():Unit @cpsParam[Unit,Unit] = { }
// def succ() = shift { (cont: Unit => Unit) => cont() }

succ()(上記の)両方のバージョンが機能します。これは、ことが判明shift面白い署名を持ち、かつもののsucc()何もしません、それはタイプのバランスのためにその署名を持っている必要があります。

reset {
  back { println() }
  val x = from0to10()
  if ((x&1)==0) {
    succ()
  } else {
    fail()
  }
  print(s"$x ")
}

予想通り、印刷します

0 2 4 6 8 10

関数内でsucc()は、必要ありません:

def onTrue(b:Boolean) = {
  if(!b) {
    fail()
  }
}
reset {
  back { println() }
  val x = from0to10()
  onTrue ((x&1)==0)
  print(s"$x ")
}

再び、それは印刷します

0 2 4 6 8 10

さて、次のように定義onOdd()しましょうonEven()

// negation: the hard way
class ControlTransferException extends Exception {}
def onOdd(x:Int) = shift { (cont: Unit => Unit) =>
  try {
    reset {
      onEven(x)
      throw new ControlTransferException() // return is not allowed here
    }
    cont()
  } catch {
    case e: ControlTransferException =>
    case t: Throwable => throw t
  }
}
reset {
  back { println() }
  val x = from0to10()
  onOdd(x)
  print(s"$x ")
}

上記でxは、偶数の場合、例外がスローされ、継続は呼び出されません。場合はx奇数で、例外がスローされていないと継続が呼び出されます。上記のコードは次のように出力します。

1 3 5 7 9 
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.