[scala] Scala 연속 형이란 무엇이며 왜 사용합니까?

방금 Scala 프로그래밍을 마쳤고 Scala 2.7과 2.8 사이의 변경 사항을 조사했습니다. 가장 중요한 것으로 보이는 것은 continuations 플러그인이지만 그것이 무엇에 유용하고 어떻게 작동하는지 이해하지 못합니다. 나는 이것이 비동기 I / O에 좋다는 것을 보았지만 그 이유를 알 수 없었습니다. 이 주제에 대해 더 많이 사용되는 리소스는 다음과 같습니다.

그리고 Stack Overflow에 대한이 질문 :

불행히도 이러한 참조 중 어느 것도 연속이 무엇인지 또는 시프트 / 리셋 기능이 수행해야하는 작업을 정의하려고하지 않으며 해당 참조를 찾지 못했습니다. 링크 된 기사의 예제가 어떻게 작동하는지 (또는 어떤 역할을하는지) 추측 할 수 없었기 때문에 도움이되는 한 가지 방법은 해당 샘플 중 하나를 한 줄씩 살펴 보는 것입니다. 세 번째 기사에서이 간단한 것조차 :

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

결과가 8 인 이유는 무엇입니까? 시작하는 데 도움이 될 것입니다.



답변

블로그 는 무엇을 reset하고 무엇을하는지 설명하고 shift있으므로 다시 읽어보고 싶을 것입니다.

내 블로그에서도 지적하는 또 다른 좋은 소스는 연속 전달 스타일 에 대한 Wikipedia 항목 입니다 . 그것은 Scala 구문을 사용하지 않고 연속이 명시 적으로 전달되지만 주제에서 가장 명확합니다.

내 블로그에 링크했지만 깨진 것처럼 보이는 구분 된 연속에 대한 논문은 많은 사용 예를 제공합니다.

그러나 구분 된 연속 개념 의 가장 좋은 예 는 Scala Swarm 이라고 생각합니다 . 그 안에서 라이브러리 는 한 지점에서 코드 실행을 중지 하고 나머지 계산은 연속이됩니다. 그런 다음 라이브러리는 작업을 수행합니다.이 경우 계산을 다른 호스트로 전송하고 결과 (액세스 된 변수의 값)를 중지 된 계산에 반환합니다.

지금, 당신은 그렇게 스칼라 페이지도 간단한 예제를 이해하지 않는 내 블로그를 참조하십시오. 그것에서 난 단지 결과가 이유의 이러한 기본 설명과 관련 8.


답변

기존 설명이 내가 바라는 것보다 개념 설명에 덜 효과적이라는 것을 알았습니다. 나는 이것이 명확하고 정확하기를 바랍니다. 나는 아직 연속을 사용하지 않았습니다.

연속 함수 cf가 호출 될 때 :

  1. 실행은 shift블록 의 나머지 부분을 건너 뛰고 끝에서 다시 시작됩니다.
    • 전달 된 매개 변수는 실행이 계속 될 cfshift블록이 “평가”하는 것입니다. 이것은 모든 호출마다 다를 수 있습니다.cf
  2. 실행은 reset블록 이 끝날 때까지 (또는 reset블록이없는 경우 호출 할 때까지 ) 계속됩니다.
    • reset블록 의 결과 (또는 reset블록이없는 경우 ()에 대한 매개 변수 )가 cf반환됩니다.
  3. 블록 cf이 끝날 때까지 실행이 계속 shift됩니다.
  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


답변

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

스칼라 플러그인 변형 예되도록 (의 입력 인수 내의 연산 reset각각에서 시작) shift의 호출로 reset되어 교환 기능 (예로 f행) 입력 shift.

대체 된 계산은 함수로 이동 (즉, 이동)됩니다 k. 이 함수는 함수를 f입력합니다 k. 여기 k 에는 대체 된 계산, k입력 x: Intk대체 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)
  }
}

나는 이것이이 두 가지 예의 이전 발표에 의해 다소 난독 화 된 일관된 공통 추상을 해명하기를 바랍니다. 예를 들어, 정식 첫 번째 예제는 내 이름이 아닌 익명 함수로 연구 논문 에 제시 f되었으므로 일부 독자에게는 빌린 두 번째 예제 read와 추상적으로 유사하다는 것이 즉시 명확하지 않았습니다 .

따라서 구분 된 연속은 “당신이 나를 외부에서 부르는 것 reset“에서 “내가 당신을 안으로 부르는 것”으로 의 제어 반전의 환상을 만듭니다 reset.

의 반환 형식을 참고 f하지만 k,의 반환 형식과 동일 할 필요는 없습니다 reset즉, f에 대한 어떤 반환 형식을 선언 할 수있는 자유가 k긴만큼 f으로 돌아 동일한 유형 reset. 저두에 대한 readcapture(참조 ENV아래).


구분 연속성을 상태, 예를 들면의 제어 반전하지 암시 적으로 수행 read하고 callback순수 기능하지 않습니다. 따라서 호출자는 참조 적으로 투명한 표현식을 생성 할 수 없으므로 의도 된 명령 적 의미에 대한 선언적 (투명한) 제어 권한 이 없습니다 .

구분 된 연속으로 순수 함수를 명시 적으로 달성 할 수 있습니다.

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의 전역 유형 추론이 없으므로 unitHaskell의 전역 (Hindley-Milner) 유형 추론 때문에 상태 모나드 (명시 적 환경을 숨기는 하나의 가능한 전략)에 대한 암시 적 해제를 지원할 수 없었습니다. 다이아몬드 다중 가상 상속을 지원하지 않는 것에 달려 있습니다 .


답변

연속은 나중에 호출 될 계산 상태를 캡처합니다.

시프트 표현식을 떠나는 것과 리셋 표현식을 함수로 남겨 두는 것 사이의 계산을 생각해보십시오. 시프트 표현식 내에서이 함수는 k라고 부르며 연속입니다. 전달하거나 나중에 호출 할 수 있으며 한 번 이상 사용할 수 있습니다.

리셋 표현식에 의해 반환 된 값은 => 뒤에있는 시프트 표현식 내부의 표현식 값이라고 생각하지만 이것에 대해서는 잘 모르겠습니다.

따라서 연속으로 함수에서 다소 임의적이고 로컬이 아닌 코드를 래핑 할 수 있습니다. 이것은 코 루틴 또는 역 추적과 같은 비표준 제어 흐름을 구현하는 데 사용할 수 있습니다.

따라서 연속은 시스템 수준에서 사용해야합니다. 애플리케이션 코드를 통해이를 뿌리는 것은 악몽에 대한 확실한 레시피가 될 것입니다. goto를 사용하는 최악의 스파게티 코드보다 훨씬 더 나쁩니다.

면책 조항 : 저는 Scala의 연속에 대해 깊이 이해하지 못했습니다. 단지 예제를보고 Scheme에서 연속을 아는 것으로 추론했습니다.


답변

내 관점에서 가장 좋은 설명은 다음과 같습니다. http://jim-mcbeath.blogspot.ru/2010/08/delimited-continuations.html

예 중 하나 :

제어 흐름을 좀 더 명확하게보기 위해 다음 코드 조각을 실행할 수 있습니다.

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


답변

– 또 다른 (최근 2016 년 5 월) 스칼라 연속 요청에 대한 기사입니다 :
스칼라의 시간 여행 : CPS 스칼라 (Scala의 계속) “에 의해
Shivansh 스리 바스타 바 ( shiv4nsh) .
또한 Dmitry Bespalov답변에 언급 된 Jim McBeath기사를 참조 합니다.

그러나 그 전에 다음과 같이 연속을 설명합니다.

연속은 컴퓨터 프로그램의 제어 상태를 추상적으로 표현한 것입니다 .
이것이 실제로 의미하는 것은 프로세스 실행의 주어진 지점에서 계산 프로세스를 나타내는 데이터 구조라는 것입니다. 생성 된 데이터 구조는 런타임 환경에서 숨겨지는 대신 프로그래밍 언어로 액세스 할 수 있습니다.

더 설명하기 위해 우리는 가장 고전적인 예를들 수 있습니다.

냉장고 앞 주방에서 샌드위치를 ​​생각하고 있다고 가정 해 보겠습니다. 당신은 바로 거기에 계속해서 그것을 주머니에 넣습니다.
그런 다음 냉장고에서 칠면조와 빵을 꺼내 카운터에 앉아 샌드위치를 ​​만드십시오.
주머니 속의 연속을 불러 내고 다시 냉장고 앞에 서서 샌드위치에 대해 생각합니다. 하지만 다행스럽게도 카운터에 샌드위치가 있고 그것을 만드는 데 사용 된 모든 재료가 사라졌습니다. 그래서 당신은 그것을 먹습니다. 🙂

이 설명에서 sandwichis는 프로그램 데이터 (예 : 힙의 객체 )의 일부이며, ” make sandwich“루틴을 호출 한 다음 반환 make sandwich with current continuation하는 대신 샌드위치를 ​​만든 다음 실행을 계속 하는 ” “루틴을 호출 한 사람을 반환합니다. 중단되었습니다.

즉, 2014 년 4 월 Scala 2.11.0-RC1에 대해 발표 된대로

우리는 scala-swing , scala-continuations 모듈을 인계받을 메인테이너를 찾고 있습니다 .
2.12는 새로운 관리자가 발견되지 않으면 포함하지 않을 것 입니다.
우리는 다른 모듈 (scala-xml, scala-parser-combinators)을 계속 유지할 것입니다.하지만 여전히 도움을 주시면 감사하겠습니다.


답변

의미있는 예제를 통한 스칼라 연속

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. 호출 된 코드에 전달 된 매개 변수 중 하나는 호출 코드의 어떤 부분이 아직 실행되지 않았는지 보여주는 반환 주소입니다 (**). 호출 코드의 해당 부분은 연속 입니다. 호출 된 코드는 해당 매개 변수로 제어를 전달하거나 무시하거나 여러 번 호출 할 수 있습니다. 여기서는 from0to100..10 범위의 각 정수에 대한 연속을 호출합니다.

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

그러나 계속은 어디에서 끝날까요? return연속 의 마지막 이 호출 된 코드로 제어를 반환 하기 때문에 이것은 중요 합니다 from0to10. Scala에서는 reset블록이 끝나는 곳 (*)에서 끝납니다.

이제 연속이 cont: Int => Unit. 왜? 우리는 호출 from0to10val x = from0to10(), 그리고 Int에 간다 값의 유형이다 x. Unit이후 블록 reset은 값을 반환하지 않아야 함을 의미합니다 (그렇지 않으면 유형 오류가 발생 함). 일반적으로 함수 입력, 연속 입력, 연속 결과, 함수 결과의 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

글쎄, 이제는 두뇌 트위스터를 할 때입니다. 의 두 가지 호출이 from0to10있습니다. 첫 번째 연속은 무엇입니까 from0to10? 그것은의 호출은 다음과 from0to10에서 바이너리 코드를 하지만, 소스 코드에서 또한 할당 문을 포함한다 val i =. 그것은 reset블록 이 끝나는 곳에서 끝나지만 블록의 끝은 reset제어를 첫 번째 from0to10. 의 최종 reset블록 반환 2에 제어를 from0to10차례로 결국 제어를 반환, back하고는 back반환의 첫 번째 호출을 제어하는 from0to10. 첫 번째 (예! 첫 번째!)가 from0to10종료되면 전체 reset블록이 종료됩니다.

제어를 되 돌리는 이러한 방법을 역 추적 이라고하며 , 이는 적어도 Prolog 및 AI 지향 Lisp 파생물 시대부터 알려진 매우 오래된 기술입니다.

이름 resetshift잘못된 이름입니다. 이러한 이름은 비트 연산을 위해 남겨 두는 것이 좋습니다. reset연속 경계를 정의 shift하고 호출 스택에서 연속을 가져옵니다.

메모)

(*) Scala에서 연속은 reset블록이 끝나는 곳에서 끝납니다. 또 다른 가능한 접근 방식은 함수가 끝나는 곳에서 끝나도록하는 것입니다.

(**) 호출 된 코드의 매개 변수 중 하나는 호출 코드의 어떤 부분이 아직 실행되지 않았는지 보여주는 반환 주소입니다. 음, 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

두 가지 중요한 작업을 고려해 보겠습니다. 연속을 버리고 ( 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