Продолжение (информатика): различия между версиями
[отпатрулированная версия] | [отпатрулированная версия] |
Понятие |
Gromolyak (обсуждение | вклад) Метки: с мобильного устройства через мобильное приложение через приложение для Android |
||
(не показана 31 промежуточная версия 20 участников) | |||
Строка 1: | Строка 1: | ||
{{Другие значения|Продолжение}} |
|||
'''Продолжение''' ({{lang-en|continuation}}) представляет состояние [[Компьютерная программа|программы]] в определённый момент, которое может быть сохранено и использовано для перехода в это состояние. Продолжения содержат всю информацию, чтобы продолжить выполнения программы с определённой точки. Состояние глобальных переменных обычно не сохраняется, однако для функциональных языков это несущественно (выборочное сохранение/восстановление значений глобальных объектов в Scheme достигается отдельным механизмом dynamic-wind). Продолжения похожи на <code>[[goto]]</code> Бейсика или <code>[[setjmp]]/[[longjmp]]</code> Си, так как также позволяют перейти в любое место программы. Но продолжения, в отличие от <code>goto</code>, позволяют перейти только в участок программы с определённым состоянием, которое должно быть сохранено заранее, в то время, как <code>goto</code> позволяет перейти в участок программы с неинициализированными [[переменная (программирование)|переменными]]. |
|||
{{Парадигмы программирования}} |
|||
'''Продолжение''' ({{lang-en|continuation}}) — абстрактное представление состояния [[Компьютерная программа|программы]] в определённый момент, которое может быть сохранено и использовано для перехода в это состояние. Продолжения содержат всю информацию, чтобы продолжить выполнения программы с определённой точки; состояние глобальных переменных обычно не сохраняется, однако для функциональных языков это несущественно (например, выборочное сохранение и восстановление значений глобальных объектов в [[Scheme]] достигается отдельным механизмом dynamic-wind). Продолжения похожи на <code>[[goto]]</code> [[Бейсик]]а или макросы <code>[[setjmp]]</code> и <code>[[longjmp]]</code> в [[Си (язык программирования)|Си]], так как так же позволяют перейти в любое место программы. Но продолжения, в отличие от <code>goto</code>, позволяют перейти в участок программы только с определённым состоянием, которое должно быть сохранено заранее, в то время, как <code>goto</code> позволяет перейти в участок программы с неинициализированными [[переменная (программирование)|переменными]]. |
|||
Первый язык, реализовавший концепцию продолжений — [[Scheme (язык программирования)|Scheme]], позднее встроенная поддержка продолжений появилась в ряде других языков. |
|||
[[Scheme (язык программирования)|Scheme]] была первым промышленным языком, в котором реализованы полноценные продолжения. Bruce Duba ввёл продолжения в [[Standard ML]]. |
|||
== |
== Определения == |
||
Формально, <code>callcc</code> — это [[функция высшего порядка]], позволяющая абстрагировать динамический контекст имеющейся функции в виде другой функции, которая и называется «продолжением». |
|||
Более наглядно, продолжение — это «вся оставшаяся часть программы от данной точки», или «функция, которая никогда не возвращает управление в точку своего вызова»{{sfn|Fake threads|1999}}. В курсах функционального программирования объяснение понятия продолжения часто сводится к «расширению (усложнению) понятия [[Сопрограмма|сопрограммы]]», но в дидактическом смысле такое объяснение считается бесполезным{{sfn|Fake threads|1999}}. Причина трудности объяснения концепции заключается в том, что продолжения фактически являются альтернативным обоснованием понятия «поведения» («вызова» в самом широком понимании), то есть несут иную семантическую модель, и в этом смысле начальный переход от «обычного» функционального программирования к программированию с интенсивным использованием продолжений можно сравнить с начальным переходом от [[Императивное программирование|императивного]] программирования к [[Функциональное программирование|функциональному]]. |
|||
Формально, <code>callcc</code> — это [[функция высшего порядка]], позволяющая абстрагировать динамический контекст имеющейся функции в виде другой функции, которая и называется «''продолжением''». |
|||
Продолжения обеспечивают математическое обоснование всего [[Порядок выполнения|порядка выполнения]] программы, от <code>goto</code> и [[Цикл (программирование)|циклов]] до [[рекурсия|рекурсии]], [[Обработка исключений|исключений]], [[Генератор (программирование)|генераторов]], [[Сопрограмма|сопрограмм]] и [[Поиск с возвратом|механизма возврата]]{{sfn|Fake threads|1999}}. Как следствие, они позволяют ''реализовать'' все эти элементы в языке посредством единой конструкции. |
|||
Более наглядно, продолжение — это «''вся оставшаяся часть программы от данной точки''», или «''функция, которая '''никогда''' не возвращает управление в точку своего вызова''»{{sfn|Fake threads}}. В процессе изучения [[Функциональное программирование|функционального программирования]] многие испытывают трудности с пониманием сущности продолжений. Традиционное объяснение этого понятия сводится к «''расширению (усложнению) понятия [[Сопрограмма|сопрограммы]]''», но в педагогическом смысле такое объяснение считается бесполезным{{sfn|Fake threads}}. Причина трудности понимания заключается том, что продолжения фактически представляют собой альтернативное ''обоснование'' понятия «поведения» («вызова» в самом широком понимании), т.е. иную семантическую модель, и в этом смысле начальный переход от «обычного» функционального программирования к программированию с интенсивным использованием продолжений можно сравнить с начальным переходом от [[Императивное программирование|императивного]] программирования к [[Функциональное программирование|функциональному]]. |
|||
Продолжения представляют собой математическое обоснование всего [[Порядок выполнения|порядка выполнения]] программы, от <code>goto</code> и [[Цикл (программирование)|циклов]] до [[рекурсия|рекурсии]], [[Обработка исключений|исключений]], {{iw|Генератор (программирование)|генераторов|en|Generator (computer programming)}}, [[Сопрограмма|сопрограмм]] и [[Поиск с возвратом|механизма возврата]]{{sfn|Fake threads}}. Как следствие, они позволяют ''реализовать'' все эти элементы в языке посредством единой конструкции. |
|||
== Программирование в стиле передачи продолжений == |
== Программирование в стиле передачи продолжений == |
||
{{Falseredirect|Программирование в стиле передачи продолжений}} |
|||
Программирование в стиле передачи продолжений ({{lang-en|continuation-passing style, CPS}}) — это [[стиль программирования]], при котором [[Порядок выполнения|передача управления]] осуществляется через механизм продолжений. Стиль CPS впервые представили {{iw|Сассман, Джеральд|Джеральд Джей Сассман|en|Gerald Jay Sussman}} и {{iw|Стил, Гай Л. мл.|Гай Стил-младший|en|Guy L. Steele, Jr.}}, одновременно с языком [[Scheme (язык программирования)|Scheme]]. |
|||
Программу в «классическом стиле» зачастую можно переписать в стиле передачи продолжений. Например, для задачи вычисления гипотенузы прямоугольного треугольника с «классическим» кодом на [[Haskell]]: |
|||
{{main|Continuation-passing style|l1=Программирование в стиле передачи продолжений {{ref-en}}}} |
|||
<source lang="haskell"> |
|||
pow2 :: Float -> Float |
|||
pow2 a = a ** 2 |
|||
add :: Float -> Float -> Float |
|||
[[Программирование в стиле передачи продолжений]] ({{lang-en|Continuation-passing style, '''CPS'''}}) — это стиль программирования, при котором [[Порядок выполнения|передача управления]] осуществляется через механизм продолжений. Стиль CPS впервые представили {{iw|Сассман, Джеральд|Джеральд Джей Сассман|en|Gerald Jay Sussman}} и {{iw|Стил, Гай Л. мл.|Гай Стил мл.|en|Guy L. Steele, Jr.}}, одновременно с языком [[Scheme (язык программирования)|Scheme]]. |
|||
add a b = a + b |
|||
pyth :: Float -> Float -> Float |
|||
Такой стиль весьма затруднителен для использования человеком, и обычно является стратегией оптимизации, применяемой для языков, гарантирующих [[хвостовая рекурсия|хвостовую рекурсию]] ([[Scheme]], [[ML]], [[Haskell]]). |
|||
pyth a b = sqrt (add (pow2 a) (pow2 b)) |
|||
</source> |
|||
можно добавить один аргумент типа <code>F</code>, где <code>F</code> означает функцию из возвращаемого значения исходной функции в произвольный тип <code>x</code>, а возвращающим значением сделать этот произвольный тип <code>x</code>: |
|||
Можно заметить, что в чистом CPS фактически не существует самих продолжений — всякий вызов оказывается [[Хвостовая рекурсия|хвостовым]]. Если язык не гарантирует оптимизацию хвостовых вызовов ({{lang-en|Tail call optimization, TCO}}), то CPS приводит к лавинообразному росту затрат памяти, т.к. при каждом вложенном вызове <code>callcc</code> растёт и само продолжение, и [[стек вызовов|стек вызовов]]. Обычно это нежелательно, но временами используется интересным способом (см. {{iw|Chicken (реализация языка Scheme)|компилятор Chicken Scheme|en|Chicken (Scheme implementation)}}). Совместное использование стратегий оптимизации TCO и CPS позволяет полностью устранить динамический стек из исполнимой программы. Ряд компиляторов [[Функциональное программирование|функциональных языков]] работает именно таким образом, к примеру компилятор SML/NJ для языка [[Standard ML]]. |
|||
<source lang="haskell"> |
|||
pow2' :: Float -> (Float -> a) -> a |
|||
pow2' a cont = cont (a ** 2) |
|||
add' :: Float -> Float -> (Float -> a) -> a |
|||
add' a b cont = cont (a + b) |
|||
-- типы a -> (b -> c) и a -> b -> c эквивалентны, поэтому CPS-функцию можно |
|||
-- рассмотреть как функцию высшего порядка от одного аргумента |
|||
sqrt' :: Float -> ((Float -> a) -> a) |
|||
sqrt' a = \cont -> cont (sqrt a) |
|||
pyth' :: Float -> Float -> (Float -> a) -> a |
|||
pyth' a b cont = pow2' a (\a2 -> pow2' b (\b2 -> add' a2 b2 (\anb -> sqrt' anb cont))) |
|||
</source> |
|||
В функции <code>pyth'</code> вычисляется квадрат от <code>a</code>, и в качестве продолжения передаётся функция ([[лямбда-выражение]]), принимающая единственным аргументом <code>a</code> в квадрате. Далее таким же образом вычисляются все последующие промежуточные значения. Для того, чтобы произвести вычисления, в качестве продолжения необходимо передать функцию от одного аргумента, например, функцию <code>id</code>, которая возвращает любое переданное ей значение. Таким образом, выражение <code>pyth' 3 4 id</code> эквивалентно <code>5.0</code>. |
|||
Стандартная haskell-библиотека в модуле <tt>Control.Monad.Cont</tt> содержит тип <code>Cont</code>, позволяющий использовать CPS функции в монаде. Функция <code>pyth'</code> будет выглядеть следующим образом: |
|||
<source lang="haskell"> |
|||
pow2_m :: Float -> Cont a Float |
|||
pow2_m a = return (a ** 2) |
|||
-- функция cont поднимает обычные CPS функции в монаду |
|||
pyth_m :: Float -> Float -> Cont a Float |
|||
pyth_m a b = do |
|||
a2 <- pow2_m a |
|||
b2 <- pow2_m b |
|||
anb <- cont (add' a2 b2) |
|||
r <- cont (sqrt' anb) |
|||
return r |
|||
</source> |
|||
Также данный модуль содержит функцию <code>callCC</code>, имеющую тип <code>MonadCont m => ((a -> m b) -> m a) -> m a</code>. Из типа видно, что она принимает единственным аргументом функцию, которая, в свою очередь, также имеет единственным аргументом функцию, прерывающую дальнейшие вычисления. Например, мы можем прервать дальнейшие вычисления, если хотя бы один из аргументов отрицательный: |
|||
<source lang="haskell"> |
|||
pyth_m :: Float -> Float -> Cont a Float |
|||
pyth_m a b = callCC $ \exitF -> do |
|||
when (b < 0 || a < 0) (exitF 0.0) -- when :: Applicative f => Bool -> f () -> f () |
|||
a2 <- pow2_m a |
|||
b2 <- pow2_m b |
|||
anb <- cont (add' a2 b2) |
|||
r <- cont (sqrt' anb) |
|||
return r |
|||
</source> |
|||
Примеры CPS в Scheme: |
|||
{| |
|||
!<center>Direct style</center>!!<center>Continuation passing style</center> |
|||
|-valign="top" |
|||
|<source lang=scheme> |
|||
(define (pyth x y) |
|||
(sqrt (+ (* x x) (* y y))))</source> |
|||
|| |
|||
<source lang=scheme> |
|||
(define (pyth& x y k) |
|||
(*& x x (lambda (x2) |
|||
(*& y y (lambda (y2) |
|||
(+& x2 y2 (lambda (x2py2) |
|||
(sqrt& x2py2 k)))))))) |
|||
</source> |
|||
|-valign="top" |
|||
| |
|||
<source lang=scheme> |
|||
(define (factorial n) |
|||
(if (= n 0) |
|||
1 ; NOT tail-recursive |
|||
(* n (factorial (- n 1))))) |
|||
</source> |
|||
|| |
|||
<source lang=scheme> |
|||
(define (factorial& n k) |
|||
(=& n 0 (lambda (b) |
|||
(if b ; продолжение растёт |
|||
(k 1) ; в рекурсивном вызове |
|||
(-& n 1 (lambda (nm1) |
|||
(factorial& nm1 (lambda (f) |
|||
(*& n f k))))))))) |
|||
</source> |
|||
|-valign="top" |
|||
| |
|||
<source lang=scheme> |
|||
(define (factorial n) |
|||
(f-aux n 1)) |
|||
(define (f-aux n a) |
|||
(if (= n 0) |
|||
a ; tail-recursive |
|||
(f-aux (- n 1) (* n a)))) |
|||
</source> |
|||
|| |
|||
<source lang=scheme> |
|||
(define (factorial& n k) (f-aux& n 1 k)) |
|||
(define (f-aux& n a k) |
|||
(=& n 0 (lambda (b) |
|||
(if b ; продолжение сохраняется |
|||
(k a) ; в рекурсивном вызове |
|||
(-& n 1 (lambda (nm1) |
|||
(*& n a (lambda (nta) |
|||
(f-aux& nm1 nta k))))))))) |
|||
</source> |
|||
|} |
|||
В «чистом» CPS фактически не существует самих продолжений — всякий вызов оказывается [[Хвостовая рекурсия|хвостовым]]. Если язык не гарантирует [[Оптимизация хвостовой рекурсии|оптимизации хвостовых вызовов]] ({{lang-en|Tail call optimization, TCO}}), то при каждом вложенном вызове <code>callcc</code> растёт и само продолжение, и [[стек вызовов]]. Обычно это нежелательно, но временами используется интересным способом (например, в {{iw|Chicken (реализация языка Scheme)|компиляторе Chicken Scheme|en|Chicken (Scheme implementation)}}). Совместное использование стратегий оптимизации TCO и CPS позволяет полностью устранить динамический стек из исполнимой программы. Ряд компиляторов [[Функциональное программирование|функциональных языков]] работает именно таким образом, к примеру, компилятор SML/NJ для языка [[Standard ML]]. |
|||
== Ограниченные и неограниченные продолжения == |
== Ограниченные и неограниченные продолжения == |
||
Существует несколько разновидностей продолжений. Наиболее |
Существует несколько разновидностей продолжений. Наиболее распространённая из них — '''''неограниченные продолжения''''' ({{lang-en2|undelimited continuations}}), реализуемые с помощью функции <code>[[callcc|call/cc]]</code> или её аналогов. Такие продолжения действительно представляют собой состояние всей программы (или одной её нити) в определённый момент. Вызов такого продолжения не похож на вызов функции, поскольку он соответствует «прыжку» в сохраненное состояние программы и не возвращает никакого значения; такое продолжение обычно нельзя вызвать несколько раз. '''''Ограниченные продолжения''''' ({{lang-en2|delimited continuations}}) абстрагируют зависимость результата некоторого блока программы от результата некоторого подвыражения этого блока. В определённом смысле они соответствуют некоторому сегменту стека вызовов, а не всему стеку. Такие продолжения могут использоваться как функции, вызываться несколько раз и так далее. Они абстрагируются с помощью механизма <code>shift/reset</code>: <code>reset</code> оборачивает внешний блок, <code>shift</code> действует как <code>[[callcc|call/cc]]</code>, но получает в качестве аргумента не глобальное продолжение, а ограниченное — зависимость значения блока reset от значения на месте блока shift. Существуют и другие разновидности, к примеру <code>prompt/control</code>. |
||
== Поддержка языками программирования == |
== Поддержка языками программирования == |
||
Многие языки программирования предоставляют эту возможность под различными именами, например: |
Многие языки программирования предоставляют эту возможность под различными именами, например: |
||
{{кол}} |
|||
* [[Scheme]]: <code>call/cc</code> (краткая запись для <code>call-with-current-continuation</code>) |
|||
* [[Scheme]]: <code>call/cc</code> (краткая запись для <code>call-with-current-continuation</code>); |
|||
* [[Standard ML]]: <code>SMLofNJ.Cont.callcc</code> |
|||
* [[Standard ML]]: <code>SMLofNJ.Cont.callcc</code>, также реализовано в [[Concurrent ML]]; |
|||
* [[Си (язык программирования)|Си]]: <code>[[setcontext]]</code> et al. ([[UNIX System V]] и [[GNU]] libc) |
|||
* [[Си (язык программирования)|Си]]: <code>[[setcontext]]</code> и аналоги ([[UNIX System V]] и [[GNU]] [[libc]]); |
|||
* [[Ruby]]: <code>callcc</code> |
|||
* [[Ruby]]: <code>callcc</code>; |
|||
* [[Smalltalk]]: <code>Continuation currentDo:</code>, в большинстве современных реализаций продолжения могут быть реализованы на чистом Smalltalk, не требуя специальной поддержки в [[виртуальная машина|виртуальной машине]]. |
|||
* [[Smalltalk]]: <code>Continuation currentDo:</code>, в большинстве современных реализаций продолжения могут быть реализованы на чистом Smalltalk, не требуя специальной поддержки в [[виртуальная машина|виртуальной машине]]; |
|||
* [[Rhino]] : <code>Continuation</code> |
|||
* [[ |
* [[JavaScript]]: <code>await</code> и <code>yield</code>; |
||
* JavaScript [[Rhino]]: <code>Continuation</code>; |
|||
* [[Factor (язык программирования)|Factor]] : <code>callcc0</code> и <code>callcc1</code> |
|||
* [[ |
* [[Haskell]]: <code>callCC</code> (в модуле <code>Control.Monad.Cont</code>); |
||
* [[ |
* [[Factor (язык программирования)|Factor]]: <code>callcc0</code> и <code>callcc1</code>; |
||
* [[Python]]: <code>yield</code>; |
|||
* [[PHP]]: Есть поддержка. |
|||
* Python [[PyPy]]: <code>_continuation.continulet</code>; |
|||
* [[C Sharp|C#]]: конструкции <code>yield return</code> и <code>await</code>. |
|||
* [[Kotlin]]: <code>suspend</code>, на основе которого реализованы <code>async</code>, <code>await</code>, <code>yield</code> и некоторые другие [[Сопрограмма|сопрограммные]] конструкции. |
|||
В любом языке, поддерживающем [[Замыкание (программирование)|замыкания]], можно писать программы [[Программирование в стиле передачи продолжений|в стиле передачи продолжений]] ({{lang-en|Continuation-passing style, CPS}}) и вручную реализовать <code>[[callcc|call/cc]]</code>. В частности это принятая практика в [[Haskell]], где легко строятся «монады, передающие продолжения» (для примера, монада <code>Cont</code> и трансформер монад <code>ContT</code> библиотеки <code>mtl</code>). |
|||
* [[Scala (язык программирования)|Scala]]: существует плагин для поддержки ограниченных продолжений; |
|||
* [[PHP]]: <code>yield</code>; |
|||
== См. также == |
|||
* [[C Sharp|C#]]: <code>yield return</code> и <code>await</code>. |
|||
* [[Замыкание (программирование)|Замыкание]] |
|||
{{конец кол}} |
|||
* [[Сопрограмма]] |
|||
В любом языке, поддерживающем [[Замыкание (программирование)|замыкания]], можно писать программы в стиле передачи продолжений и вручную реализовать <code>[[callcc|call/cc]]</code>. В частности, это принятая практика в [[Haskell]], где легко строятся «монады, передающие продолжения» (для примера, монада <code>Cont</code> и трансформер монад <code>ContT</code> библиотеки <code>mtl</code>). |
|||
== Примечания == |
== Примечания == |
||
Строка 48: | Строка 162: | ||
== Ссылки == |
== Ссылки == |
||
* [http://www.smalltalk.ru/articles/web-continuations.html Продолжение всемирной паутины] |
* [http://www.smalltalk.ru/articles/web-continuations.html Продолжение всемирной паутины] — о использования продолжений для построения веб-приложений. |
||
* [http://fprog.ru/lib/ Библиотечка ПФП] |
* [http://fprog.ru/lib/ Библиотечка ПФП] — в статье «Паттерны использования call with current continuation» (перевод) описана концепция продолжений и дан ряд разнообразных примеров их использования. |
||
* [http://library.readscheme.org/page6.html Continuations and Continuation Passing Style] |
* [https://web.archive.org/web/20110514131059/http://library.readscheme.org/page6.html Continuations and Continuation Passing Style] — большая коллекция статей о разных видах продолжений и их использовании. |
||
* {{ |
* {{статья |
||
| |
|автор=Tim Peters |
||
| |
|заглавие=Fake threads (was <nowiki>[Python-Dev]</nowiki> ActiveState & fork & Perl) |
||
| |
|ссылка=https://mail.python.org/pipermail/python-dev/1999-July/000467.html |
||
|год=1999 |
|||
|ref=Fake threads |
|ref=Fake threads |
||
}} |
}} |
||
{{compu-lang-stub}} |
|||
[[Категория:Концепции языков программирования]] |
[[Категория:Концепции языков программирования]] |
Текущая версия от 18:43, 14 мая 2024
Продолжение (англ. continuation) — абстрактное представление состояния программы в определённый момент, которое может быть сохранено и использовано для перехода в это состояние. Продолжения содержат всю информацию, чтобы продолжить выполнения программы с определённой точки; состояние глобальных переменных обычно не сохраняется, однако для функциональных языков это несущественно (например, выборочное сохранение и восстановление значений глобальных объектов в Scheme достигается отдельным механизмом dynamic-wind). Продолжения похожи на goto
Бейсика или макросы setjmp
и longjmp
в Си, так как так же позволяют перейти в любое место программы. Но продолжения, в отличие от goto
, позволяют перейти в участок программы только с определённым состоянием, которое должно быть сохранено заранее, в то время, как goto
позволяет перейти в участок программы с неинициализированными переменными.
Первый язык, реализовавший концепцию продолжений — Scheme, позднее встроенная поддержка продолжений появилась в ряде других языков.
Определения
[править | править код]Формально, callcc
— это функция высшего порядка, позволяющая абстрагировать динамический контекст имеющейся функции в виде другой функции, которая и называется «продолжением».
Более наглядно, продолжение — это «вся оставшаяся часть программы от данной точки», или «функция, которая никогда не возвращает управление в точку своего вызова»[1]. В курсах функционального программирования объяснение понятия продолжения часто сводится к «расширению (усложнению) понятия сопрограммы», но в дидактическом смысле такое объяснение считается бесполезным[1]. Причина трудности объяснения концепции заключается в том, что продолжения фактически являются альтернативным обоснованием понятия «поведения» («вызова» в самом широком понимании), то есть несут иную семантическую модель, и в этом смысле начальный переход от «обычного» функционального программирования к программированию с интенсивным использованием продолжений можно сравнить с начальным переходом от императивного программирования к функциональному.
Продолжения обеспечивают математическое обоснование всего порядка выполнения программы, от goto
и циклов до рекурсии, исключений, генераторов, сопрограмм и механизма возврата[1]. Как следствие, они позволяют реализовать все эти элементы в языке посредством единой конструкции.
Программирование в стиле передачи продолжений
[править | править код]Программирование в стиле передачи продолжений (англ. continuation-passing style, CPS) — это стиль программирования, при котором передача управления осуществляется через механизм продолжений. Стиль CPS впервые представили Джеральд Джей Сассман[англ.] и Гай Стил-младший[англ.], одновременно с языком Scheme.
Программу в «классическом стиле» зачастую можно переписать в стиле передачи продолжений. Например, для задачи вычисления гипотенузы прямоугольного треугольника с «классическим» кодом на Haskell:
pow2 :: Float -> Float
pow2 a = a ** 2
add :: Float -> Float -> Float
add a b = a + b
pyth :: Float -> Float -> Float
pyth a b = sqrt (add (pow2 a) (pow2 b))
можно добавить один аргумент типа F
, где F
означает функцию из возвращаемого значения исходной функции в произвольный тип x
, а возвращающим значением сделать этот произвольный тип x
:
pow2' :: Float -> (Float -> a) -> a
pow2' a cont = cont (a ** 2)
add' :: Float -> Float -> (Float -> a) -> a
add' a b cont = cont (a + b)
-- типы a -> (b -> c) и a -> b -> c эквивалентны, поэтому CPS-функцию можно
-- рассмотреть как функцию высшего порядка от одного аргумента
sqrt' :: Float -> ((Float -> a) -> a)
sqrt' a = \cont -> cont (sqrt a)
pyth' :: Float -> Float -> (Float -> a) -> a
pyth' a b cont = pow2' a (\a2 -> pow2' b (\b2 -> add' a2 b2 (\anb -> sqrt' anb cont)))
В функции pyth'
вычисляется квадрат от a
, и в качестве продолжения передаётся функция (лямбда-выражение), принимающая единственным аргументом a
в квадрате. Далее таким же образом вычисляются все последующие промежуточные значения. Для того, чтобы произвести вычисления, в качестве продолжения необходимо передать функцию от одного аргумента, например, функцию id
, которая возвращает любое переданное ей значение. Таким образом, выражение pyth' 3 4 id
эквивалентно 5.0
.
Стандартная haskell-библиотека в модуле Control.Monad.Cont содержит тип Cont
, позволяющий использовать CPS функции в монаде. Функция pyth'
будет выглядеть следующим образом:
pow2_m :: Float -> Cont a Float
pow2_m a = return (a ** 2)
-- функция cont поднимает обычные CPS функции в монаду
pyth_m :: Float -> Float -> Cont a Float
pyth_m a b = do
a2 <- pow2_m a
b2 <- pow2_m b
anb <- cont (add' a2 b2)
r <- cont (sqrt' anb)
return r
Также данный модуль содержит функцию callCC
, имеющую тип MonadCont m => ((a -> m b) -> m a) -> m a
. Из типа видно, что она принимает единственным аргументом функцию, которая, в свою очередь, также имеет единственным аргументом функцию, прерывающую дальнейшие вычисления. Например, мы можем прервать дальнейшие вычисления, если хотя бы один из аргументов отрицательный:
pyth_m :: Float -> Float -> Cont a Float
pyth_m a b = callCC $ \exitF -> do
when (b < 0 || a < 0) (exitF 0.0) -- when :: Applicative f => Bool -> f () -> f ()
a2 <- pow2_m a
b2 <- pow2_m b
anb <- cont (add' a2 b2)
r <- cont (sqrt' anb)
return r
Примеры CPS в Scheme:
(define (pyth x y)
(sqrt (+ (* x x) (* y y))))
|
(define (pyth& x y k)
(*& x x (lambda (x2)
(*& y y (lambda (y2)
(+& x2 y2 (lambda (x2py2)
(sqrt& x2py2 k))))))))
|
(define (factorial n)
(if (= n 0)
1 ; NOT tail-recursive
(* n (factorial (- n 1)))))
|
(define (factorial& n k)
(=& n 0 (lambda (b)
(if b ; продолжение растёт
(k 1) ; в рекурсивном вызове
(-& n 1 (lambda (nm1)
(factorial& nm1 (lambda (f)
(*& n f k)))))))))
|
(define (factorial n)
(f-aux n 1))
(define (f-aux n a)
(if (= n 0)
a ; tail-recursive
(f-aux (- n 1) (* n a))))
|
(define (factorial& n k) (f-aux& n 1 k))
(define (f-aux& n a k)
(=& n 0 (lambda (b)
(if b ; продолжение сохраняется
(k a) ; в рекурсивном вызове
(-& n 1 (lambda (nm1)
(*& n a (lambda (nta)
(f-aux& nm1 nta k)))))))))
|
В «чистом» CPS фактически не существует самих продолжений — всякий вызов оказывается хвостовым. Если язык не гарантирует оптимизации хвостовых вызовов (англ. Tail call optimization, TCO), то при каждом вложенном вызове callcc
растёт и само продолжение, и стек вызовов. Обычно это нежелательно, но временами используется интересным способом (например, в компиляторе Chicken Scheme[англ.]). Совместное использование стратегий оптимизации TCO и CPS позволяет полностью устранить динамический стек из исполнимой программы. Ряд компиляторов функциональных языков работает именно таким образом, к примеру, компилятор SML/NJ для языка Standard ML.
Ограниченные и неограниченные продолжения
[править | править код]Существует несколько разновидностей продолжений. Наиболее распространённая из них — неограниченные продолжения (undelimited continuations), реализуемые с помощью функции call/cc
или её аналогов. Такие продолжения действительно представляют собой состояние всей программы (или одной её нити) в определённый момент. Вызов такого продолжения не похож на вызов функции, поскольку он соответствует «прыжку» в сохраненное состояние программы и не возвращает никакого значения; такое продолжение обычно нельзя вызвать несколько раз. Ограниченные продолжения (delimited continuations) абстрагируют зависимость результата некоторого блока программы от результата некоторого подвыражения этого блока. В определённом смысле они соответствуют некоторому сегменту стека вызовов, а не всему стеку. Такие продолжения могут использоваться как функции, вызываться несколько раз и так далее. Они абстрагируются с помощью механизма shift/reset
: reset
оборачивает внешний блок, shift
действует как call/cc
, но получает в качестве аргумента не глобальное продолжение, а ограниченное — зависимость значения блока reset от значения на месте блока shift. Существуют и другие разновидности, к примеру prompt/control
.
Поддержка языками программирования
[править | править код]Многие языки программирования предоставляют эту возможность под различными именами, например:
- Scheme:
call/cc
(краткая запись дляcall-with-current-continuation
); - Standard ML:
SMLofNJ.Cont.callcc
, также реализовано в Concurrent ML; - Си:
setcontext
и аналоги (UNIX System V и GNU libc); - Ruby:
callcc
; - Smalltalk:
Continuation currentDo:
, в большинстве современных реализаций продолжения могут быть реализованы на чистом Smalltalk, не требуя специальной поддержки в виртуальной машине; - JavaScript:
await
иyield
; - JavaScript Rhino:
Continuation
; - Haskell:
callCC
(в модулеControl.Monad.Cont
); - Factor:
callcc0
иcallcc1
; - Python:
yield
; - Python PyPy:
_continuation.continulet
; - Kotlin:
suspend
, на основе которого реализованыasync
,await
,yield
и некоторые другие сопрограммные конструкции. - Scala: существует плагин для поддержки ограниченных продолжений;
- PHP:
yield
; - C#:
yield return
иawait
.
В любом языке, поддерживающем замыкания, можно писать программы в стиле передачи продолжений и вручную реализовать call/cc
. В частности, это принятая практика в Haskell, где легко строятся «монады, передающие продолжения» (для примера, монада Cont
и трансформер монад ContT
библиотеки mtl
).
Примечания
[править | править код]- ↑ 1 2 3 Fake threads, 1999.
Ссылки
[править | править код]- Продолжение всемирной паутины — о использования продолжений для построения веб-приложений.
- Библиотечка ПФП — в статье «Паттерны использования call with current continuation» (перевод) описана концепция продолжений и дан ряд разнообразных примеров их использования.
- Continuations and Continuation Passing Style — большая коллекция статей о разных видах продолжений и их использовании.
- Tim Peters. Fake threads (was [Python-Dev] ActiveState & fork & Perl). — 1999.