stringtranslate.com

Стиль продолжения прохождения

В функциональном программировании стиль передачи продолжения ( CPS ) — это стиль программирования, в котором управление передается явно в форме продолжения . Это контрастирует с прямым стилем, который является обычным стилем программирования. Джеральд Джей Сассман и Гай Л. Стил-младший придумали эту фразу в AI Memo 349 (1975), в которой изложена первая версия языка программирования Scheme . [1] [2] Джон К. Рейнольдс дает подробный отчет о многочисленных открытиях продолжений. [3]

Функция, написанная в стиле передачи продолжения, принимает дополнительный аргумент: явное «продолжение»; т.е. функция одного аргумента. Когда функция CPS вычислила свое значение результата, она «возвращает» его, вызывая функцию продолжения с этим значением в качестве аргумента. Это означает, что при вызове функции CPS вызывающая функция должна предоставить вызываемой процедуре «возвращаемое» значение подпрограммы. Выражение кода в такой форме делает явными многие вещи, которые неявно подразумеваются в прямом стиле. К ним относятся: возвраты процедур, которые проявляются как вызовы продолжения; промежуточные значения, которым присвоены имена; порядок вычисления аргументов, который сделан явным; и хвостовые вызовы , которые просто вызывают процедуру с тем же неизмененным продолжением, которое было передано вызывающей стороне.

Программы могут быть автоматически преобразованы из прямого стиля в CPS. Функциональные и логические компиляторы часто используют CPS в качестве промежуточного представления , тогда как компилятор для императивного или процедурного языка программирования будет использовать статическую форму одиночного присваивания (SSA). [4] SSA формально эквивалентен подмножеству CPS (исключая нелокальный поток управления, который не возникает, когда CPS используется в качестве промежуточного представления). [5] Функциональные компиляторы также могут использовать A-нормальную форму (ANF) (но только для языков, требующих быстрой оценки), а не с помощью « thunk » (описанных в примерах ниже) в CPS. CPS чаще используется компиляторами , чем программистами, как локальный или глобальный стиль.

Примеры

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

Ключом к CPS является запоминание того, что (а) каждая функция принимает дополнительный аргумент, известный как ее продолжение, и (б) каждый аргумент в вызове функции должен быть либо переменной, либо лямбда-выражением (а не более сложным выражением). Это приводит к переворачиванию выражений «наизнанку», поскольку сначала должны быть вычислены самые внутренние части выражения, таким образом, CPS четко определяет порядок вычисления, а также поток управления. Некоторые примеры кода в прямом стиле и соответствующие CPS приведены ниже. Эти примеры написаны на языке программирования Scheme ; по соглашению функция продолжения представлена ​​как параметр с именем " k":

Обратите внимание, что в версиях CPS используемые примитивы, как +&и *&сами CPS, а не прямой стиль, поэтому, чтобы приведенные выше примеры работали в системе Scheme, нам нужно будет написать эти версии примитивов CPS, например, *&определенные следующим образом:

( определить ( *& x y k ) ( k ( * x y )))        

В общем, чтобы сделать это, мы могли бы написать процедуру преобразования:

( define ( cps-prim f ) ( лямбда- аргументы ( let (( r ( обратные аргументы ))) (( car r ) ( Apply f ( verse ( cdr r ))))))) ( define *& ( cps-prim * )) ( определить +& ( cps-prim + ))                     

Чтобы вызвать процедуру, написанную на CPS, из процедуры, написанной в прямом стиле, необходимо предоставить продолжение, которое будет получать результат, вычисленный процедурой CPS. В приведенном выше примере (при условии, что примитивы CPS предоставлены) мы могли бы вызвать (factorial& 10 (lambda (x) (display x) (newline))).

Между компиляторами существуют некоторые различия в способах предоставления примитивных функций в CPS. Выше мы использовали простейшее соглашение, однако иногда предоставляются логические примитивы, которые требуют двух переходников для вызова в двух возможных случаях, поэтому вызов (=& n 0 (lambda (b) (if b ...)))внутри f-aux&определения выше будет записан вместо этого как (=& n 0 (lambda () (k a)) (lambda () (-& n 1 ...))). Точно так же иногда ifсам примитив не включается в CPS, а вместо него предоставляется функция if&, принимающая три аргумента: логическое условие и два преобразователя, соответствующие двум ветвям условного выражения.

Приведенные выше переводы показывают, что CPS представляет собой глобальную трансформацию. Как и следовало ожидать, факториал прямого типа принимает один аргумент; факториал& CPS принимает два аргумента: аргумент и продолжение. Любая функция, вызывающая функцию CPS, должна либо предоставить новое продолжение, либо передать свое собственное; любые вызовы функции, поддерживающей CPS, к функции, не поддерживающей CPS, будут использовать неявные продолжения. Таким образом, чтобы обеспечить полное отсутствие стека функций, вся программа должна находиться в CPS.

CPS в Haskell

В этом разделе мы напишем функцию pyth, вычисляющую гипотенузу по теореме Пифагора . Традиционная реализация функции pythвыглядит так:

pow2 :: Float -> Float pow2 x = x ** 2         add :: Float -> Float -> Float add x y = x + y            pyth :: Float -> Float -> Float pyth x y = sqrt ( add ( pow2 x ) ( pow2 y ))               

Чтобы преобразовать традиционную функцию в CPS, нам нужно изменить ее сигнатуру. Функция получит еще один аргумент типа функции, и ее тип возвращаемого значения зависит от этой функции:

pow2' :: Float -> ( Float -> a ) -> a pow2' x cont = cont ( x ** 2 )               add' :: Float -> Float -> ( Float -> a ) -> a add' x y cont = cont ( x + y )                  -- Типы a -> (b -> c) и a -> b -> c эквивалентны, поэтому функцию CPS -- можно рассматривать как функцию более высокого порядка sqrt' :: Float -> (( Float -> a ) -> a ) sqrt' x = \ cont -> cont ( sqrt x )               pyth' :: Float -> Float -> ( Float -> a ) -> a pyth' x y cont = pow2' x ( \ x2 -> pow2' y ( \ y2 -> add' x2 y2 ( \ anb -> sqrt и продолжение ) ))                              

Сначала мы вычисляем квадрат функции in pyth'и передаем лямбда-функцию в качестве продолжения, которая будет принимать квадрат a в качестве первого аргумента. И так до тех пор, пока не дойдем до результата наших расчетов. Чтобы получить результат этой функции, мы можем передать idфункцию в качестве последнего аргумента, который возвращает переданное ей значение без изменений: pyth' 3 4 id == 5.0.

Библиотека mtl, поставляемая вместе с GHC , имеет модуль Control.Monad.Cont. Этот модуль предоставляет тип Cont, который реализует Monad и некоторые другие полезные функции. В следующем фрагменте показана pyth'функция с использованием Cont:

pow2_m :: Float -> Cont a Float pow2_m a = return ( a ** 2 )            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если хотя бы один из ее аргументов отрицательный и возвращает ноль:

pyth_m :: Float -> Float -> Cont a Float pyth_m a b = callCC $ \ exitF -> do -- $ знак помогает избежать круглых скобок: a $ b + c == a (b + c), когда ( b < 0 || a < 0 ) ( exitF 0.0 ) -- When :: Аппликативный f => Bool -> f () -> f () a2 <- pow2_m a b2 <- pow2_m b anb <- cont ( add' a2 b2 ) r <- cont ( sqrt' anb ) return r                                                 

Продолжения как объекты

Программирование с использованием продолжений также может быть полезно, когда вызывающая сторона не хочет ждать завершения выполнения вызываемой программы. Например, в программировании пользовательского интерфейса (UI) процедура может настроить поля диалогового окна и передать их вместе с функцией продолжения в структуру пользовательского интерфейса. Этот вызов сразу же возвращается, позволяя коду приложения продолжать работу, пока пользователь взаимодействует с диалоговым окном. Как только пользователь нажимает кнопку «ОК», платформа вызывает функцию продолжения с обновленными полями. Хотя этот стиль кодирования использует продолжения, он не является полным CPS. [ нужны разъяснения ]

функция подтвержденияИмя () { поля . имя = имя ; рамки . Show_dialog_box ( поля , подтверждениеИмениПродолжение ); }       функция подтвержденияИмяПродолжение ( поля ) { имя = поля . имя ; }     

Аналогичную идею можно использовать, когда функция должна выполняться в другом потоке или на другом процессоре. Платформа может выполнить вызванную функцию в рабочем потоке, а затем вызвать функцию продолжения в исходном потоке с результатами рабочего потока. Это в Java 8 с использованием инфраструктуры Swing UI:

void buttonHandler () { // Это выполняется в потоке пользовательского интерфейса Swing. // Здесь мы можем получить доступ к виджетам пользовательского интерфейса, чтобы получить параметры запроса. параметр int = getField ();         new Thread (() -> { // Этот код выполняется в отдельном потоке. // Мы можем делать такие вещи, как доступ к базе данных или // блокирующему ресурсу, например сети, для получения данных. int result = поиск ( параметр );           Явакс . качать . SwingUtilities . ignoreLater (() -> { // Этот код выполняется в потоке пользовательского интерфейса и может использовать // полученные данные для заполнения виджетов пользовательского интерфейса. setField ( result ); }); }). начинать (); }       

Хвостовые вызовы

Каждый вызов в CPS является хвостовым вызовом , и продолжение передается явно. Использование CPS без оптимизации хвостового вызова (TCO) приведет к потенциальному росту во время рекурсии не только созданного продолжения, но и стека вызовов . Обычно это нежелательно, но его можно использовать интересным образом — см. компилятор Chicken Scheme . Поскольку CPS и TCO устраняют концепцию неявного возврата функции, их совместное использование может устранить необходимость в стеке времени выполнения. Некоторые компиляторы и интерпретаторы функциональных языков программирования используют эту возможность по-новому. [6]

Использование и реализация

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

Написание кода в CPS хотя и не невозможно, но часто подвержено ошибкам. Существуют различные переводы, обычно определяемые как одно- или двухпроходные преобразования чистого лямбда-исчисления , которые преобразуют выражения прямого стиля в выражения CPS. Однако писать батутным стилем чрезвычайно сложно; при использовании он обычно является целью какого-либо преобразования, например компиляции .

Функции, использующие более одного продолжения, могут быть определены для захвата различных парадигм потока управления, например (в Scheme ):

( define ( /& x y ok err ) ( =& y 0.0 ( лямбда ( b ) ( if b ( err ( список «деление на ноль!» x y )) ( ok ( / x y ))))))                     

Следует отметить, что преобразование CPS концептуально является вложением Йонеды . [7] Это также похоже на встраивание лямбда-исчисления в π-исчисление . [8] [9]

Использование в других областях

За пределами информатики CPS представляет более общий интерес как альтернатива традиционному методу составления простых выражений в сложные. Например, в рамках лингвистической семантики Крис Баркер и его коллеги предположили, что определение значений предложений с использованием CPS может объяснить определенные явления в естественном языке . [10]

В математике изоморфизм Карри -Ховарда между компьютерными программами и математическими доказательствами связывает перевод в стиле передачи продолжения с вариацией вложений двойного отрицания классической логики в интуиционистскую (конструктивную) логику . В отличие от обычного перевода с двойным отрицанием , который отображает атомарные предложения p в (( p → ⊥) → ⊥), стиль передачи продолжения заменяет ⊥ типом конечного выражения. Соответственно, результат получается путем передачи тождественной функции как продолжения выражения CPS, как в приведенном выше примере.

Классическая логика сама по себе связана с непосредственным управлением продолжением программ, как в операторе управления вызовом с текущим продолжением Scheme , наблюдении Тима Гриффина (с использованием тесно связанного оператора управления C). [11]

Смотрите также

Примечания

  1. ^ Сассман, Джеральд Джей ; Стил, Гай Л. младший (декабрь 1975 г.). «Схема: интерпретатор расширенного лямбда-исчисления»  . Памятка AI . 349 : 19. То есть в этом стиле программирования с передачей продолжения функция всегда «возвращает» свой результат, «отправляя» его другой функции . Это ключевая идея.
  2. ^ Сассман, Джеральд Джей ; Стил, Гай Л. младший (декабрь 1998 г.). «Схема: интерпретатор расширенного лямбда-исчисления» (переиздание) . Вычисления высшего порядка и символьные вычисления . 11 (4): 405–439. дои : 10.1023/А: 1010035624696. S2CID  18040106. Мы полагаем, что это первое упоминание термина « стиль продолжения-передачи » в литературе. Это оказалось важной концепцией анализа и преобразования исходного кода для компиляторов и других инструментов метапрограммирования. Он также вдохновил множество других «стилей» программного выражения.
  3. ^ Рейнольдс, Джон К. (1993). «Открытия продолжений». LISP и символьные вычисления . 6 (3–4): 233–248. CiteSeerX 10.1.1.135.4705 . дои : 10.1007/bf01019459. S2CID  192862. 
  4. ^ * Аппель, Эндрю В. (апрель 1998 г.). «SSA — это функциональное программирование». Уведомления ACM SIGPLAN . 33 (4): 17–20. CiteSeerX 10.1.1.34.3282 . дои : 10.1145/278283.278285. S2CID  207227209. 
  5. ^ * Келси, Ричард А. (март 1995 г.). «Соответствие между стилем прохождения продолжения и статической формой одиночного задания». Уведомления ACM SIGPLAN . 30 (3): 13–22. CiteSeerX 10.1.1.489.930 . дои : 10.1145/202530.202532. 
  6. ^ Аппель, Эндрю В. (1992). Компиляция с продолжениями. Издательство Кембриджского университета. ISBN 0-521-41695-7
  7. ^ Майк Стей, «Преобразование с продолжением передачи и вложение Йонеды»
  8. ^ Майк Стей, "Исчисление Пи II"
  9. ^ Будоль, Жерар (1997). «П-исчисление в прямом стиле». CiteSeerX 10.1.1.52.6034 . 
  10. ^ Баркер, Крис (1 сентября 2002 г.). «Продолжение и природа количественной оценки» (PDF) . Семантика естественного языка . 10 (3): 211–242. дои : 10.1023/А: 1022183511876. ISSN  1572-865X. S2CID  118870676.
  11. ^ Гриффин, Тимоти (январь 1990 г.). «Формула как тип понятия управления». Материалы 17-го симпозиума ACM SIGPLAN-SIGACT по принципам языков программирования - POPL '90 . Том. 17. С. 47–58. дои : 10.1145/96709.96714. ISBN 978-0-89791-343-0. S2CID  3005134.

Рекомендации