Что такое продолжение Scala и зачем их использовать? - PullRequest
84 голосов
/ 03 октября 2009

Я только что закончил Программирование в Scala , и я изучал изменения между Scala 2.7 и 2.8. Наиболее важным является плагин для продолжения, но я не понимаю, для чего он полезен или как он работает. Я видел, что это хорошо для асинхронного ввода-вывода, но я не смог выяснить, почему. Вот некоторые из наиболее популярных ресурсов по этому вопросу:

И этот вопрос о переполнении стека:

К сожалению, ни одна из этих ссылок не пытается определить, для чего предназначены продолжения или что должны делать функции сдвига / сброса, и я не нашел никаких ссылок, которые бы это делали. Я не смог догадаться, как работает какой-либо из примеров в связанных статьях (или что они делают), поэтому одним из способов помочь мне было бы построчно просмотреть один из этих примеров. Даже этот простой из третьей статьи:

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

Почему результат 8? Это, вероятно, поможет мне начать.

Ответы [ 6 ]

36 голосов
/ 03 октября 2009

Мой блог объясняет, что делают reset и shift, так что вы можете прочитать это снова.

Другим хорошим источником, о котором я также упоминаю в своем блоге, является запись в Википедии о стиле прохождения продолжения . Это, безусловно, самый ясный предмет, хотя он не использует синтаксис Scala, и продолжение явно передается.

В статье о продолжениях с разделителями, на которую я ссылаюсь в своем блоге, но, похоже, она не работает, приводится много примеров использования.

Но я думаю, что лучшим примером концепции продолжений с разделителями является Scala Swarm. В ней библиотека останавливает выполнение вашего кода в одной точке, а оставшиеся вычисления становятся продолжением. Затем библиотека делает что-то - в этом случае, передает вычисление другому хосту и возвращает результат (значение переменной, к которой был получен доступ) вычислению, которое было остановлено.

Теперь вы не понимаете даже простого примера на странице Scala, поэтому do читайте в моем блоге. В нем я только , занимающийся объяснением этих основ, почему получается 8.

31 голосов
/ 05 ноября 2009

Я обнаружил, что существующие объяснения менее эффективны при объяснении концепции, чем я надеюсь. Я надеюсь, что это ясно (и правильно). Я еще не использовал продолжения.

Когда вызывается функция продолжения cf:

  1. Выполнение пропускает остальную часть блока shift и начинается снова в конце
    • параметр, переданный в cf, - это то, что блок "1011 *" "оценивает" по мере продолжения выполнения. это может быть разным для каждого звонка на cf
  2. Выполнение продолжается до конца блока reset (или до вызова reset, если блока нет)
    • результат блока reset (или параметра reset (), если блока нет) - это то, что cf возвращает
  3. Выполнение продолжается после cf до конца shift блока
  4. Выполнение пропускается до конца блока reset (или вызова сброса?)

Итак, в этом примере следуйте буквам от А до Я

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
9 голосов
/ 05 ноября 2012

Учитывая канонический пример из исследовательской работы для продолжений 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 вводит функцию k, где k содержит замененное вычисление, k input 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)
  }
}

Я надеюсь, что это объясняет общую связную абстракцию, которая была несколько омрачена предварительным представлением этих двух примеров. Например, первый канонический пример был представлен в исследовательской работе как анонимная функция вместо моего имени f, поэтому некоторые читатели не сразу поняли, что он абстрактно аналогичен read во заимствованном втором примере.

Продолжения, разделенные таким образом, создают иллюзию инверсии контроля от «ты звонишь мне извне reset» до «я звоню тебе внутри reset».

Обратите внимание, что тип возвращаемого значения f есть, но k нет, должен совпадать с типом возвращаемого значения reset, т. Е. f может свободно объявлять любой тип возвращаемого значения для k пока f возвращает тот же тип, что и reset. То же самое для read и capture (см. Также 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, и поэтому, насколько я знаю, не удалось поддержать неявное поднятие до unit монады состояний (как одной из возможных стратегий скрытия явной среды), потому что глобальный Haskell ( Вывод типа Хиндли-Милнера) зависит от , не поддерживающего множественное виртуальное наследование алмазов .

8 голосов
/ 03 октября 2009

Продолжение фиксирует состояние вычисления, которое будет вызвано позже.

Подумайте о вычислении между выходом из выражения сдвига и оставлением выражения сброса как функции. Внутри выражения сдвига эта функция называется k, это продолжение. Вы можете передать его, вызвать позже, даже более одного раза.

Я думаю, что значение, возвращаемое выражением сброса, является значением выражения внутри выражения сдвига после =>, но в этом я не совсем уверен.

Таким образом, с продолжениями вы можете заключить в функцию довольно произвольный и нелокальный фрагмент кода. Это может быть использовано для реализации нестандартного потока управления, такого как сопрограммирование или возврат.

Таким образом, продолжения должны использоваться на системном уровне. Пролить их через код вашего приложения было бы верным рецептом для ночных кошмаров, гораздо хуже, чем худший код спагетти с использованием goto.

Отказ от ответственности: У меня нет глубокого понимания продолжений в Scala, я просто сделал вывод из рассмотрения примеров и знания продолжений из Схемы.

4 голосов
/ 19 апреля 2016

С моей точки зрения, лучшее объяснение было дано здесь: http://jim -mcbeath.blogspot.ru / 2010/08 / delimited-продолжений.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
1 голос
/ 01 августа 2016

Другая (более поздняя - май 2016 г.) статья о продолжениях Scala:
« Путешествие во времени в Scala: CPS в Scala (продолжение scala) » Шиванш Шривастава (shiv4nsh) .
Это также относится к Джиму МакБиту статье , упомянутой в Дмитрию Беспалову ответу .

Но до этого он описывает продолжение следующим образом:

Продолжением является абстрактное представление состояния управления компьютерной программой .
Так что на самом деле это означает, что это структура данных, которая представляет вычислительный процесс в данный момент в процессе выполнения; Созданная структура данных может быть доступна языку программирования, а не скрыта в среде выполнения.

Чтобы объяснить это далее, мы можем привести один из самых классических примеров,

Скажем, вы находитесь на кухне перед холодильником, думая о бутерброде. Вы берете продолжение прямо там и суете его в карман.
Затем вы достаете немного индейки и хлеба из холодильника и делаете себе бутерброд, который сейчас стоит на прилавке.
Вы вызываете продолжение в своем кармане, и вы снова стоите перед холодильником, думая о бутерброде. Но, к счастью, на прилавке есть бутерброд, и все материалы, использованные для его приготовления, исчезли. Так ты ешь это. : -)

В этом описании sandwich является частью программных данных *1034* (например, объекта в куче), и вместо вызова «make sandwich» подпрограммы с последующим возвратом человеку называется подпрограммой «make sandwich with current continuation», которая создает сэндвич и затем продолжается там, где остановилось выполнение.

При этом, как было объявлено в апреле 2014 года для Scala 2.11.0-RC1

Мы ищем сопровождающих, чтобы взять на себя следующие модули: scala-swing , scala-продолжений .
2.12 не будет включать их, если новый сопровождающий не найден .
Скорее всего, мы продолжим поддерживать другие модули (scala-xml, scala-parser-combinators), но помощь по-прежнему высоко ценится.

...