Продолжение — это состояние программы в определённый момент, которое мы потом можем использовать, чтобы вернуться в то состояние.
С помощью продолжений можно реализовать обработку исключений, подобие goto и множество других вещей напоминающих императивные конструкции.
Также, используя продолжения можно улучшить производительность программы, убирая ненужные «обёртывания» и сопоставления с образцом.
В этой статье я расскажу, как можно реализовать продолжения в Haskell, и покажу несколько интересных функций работающих с ними.
Для начала, посмотрим что такое продолжение и программирование в стиле продолжений.
Обычные функции:
А теперь в стиле продолжений:
Теперь функции кроме непосредственно аргументов принимают на вход функцию, которая будет применена к результату. Это и есть продолжение.
С помощью продолжений мы можем соединять функции, что и происходит в
Продолжения здесь имеют тип
Например, чтобы вывести результат на консоль, мы можем передать
Можно заметить некоторые закономерности в стиле продолжений.
К примеру, простые функции, такие как
А соединяли мы их так:
Всё это напоминает нам о монадах, первый пример похож на
Введём монаду Cont как:
Но почему
Дело в том, что когда мы писали функции в стиле продолжений, каждая функция брала дополнительный параметр, продолжающий вычисление.
Если мы «заполним» функцию в стиле продолжений аргументами (до самого продолжения), её тип будет
Попробуем сделать Cont монадой.
Перепишем нашу программу с использованием монады Cont:
Теперь всё выглядит намного понятней.
Перейдём к функциям, работающим с Cont.
Начнём с простого примера:
Что за бесполезная функция? Больше похоже будто это просто синоним для конструктора Cont.
Но всё не так просто. Посмотрим на тип
По настоящему,
Для чего можно использовать
Опробуем нашу программу:
Эта программа вычисляет факториал, и если он оказывается больше 9000, возвращает сообщение «OVER 9000», а если нет, то просто его значение.
Здесь мы использовали
Также возможно использование вложенных callCC-блоков:
Попробуем:
Если список имён пустой, то вызываем
Если человек один, используем «There is », если их много, то «There are: ».
Заметьте, что когда вычисление доходит до return в callCC-блоке, то результат возвращается как обычно.
Так как же реализована функция
Начинается она по обычному, с оборачивания функции в стиле продолжений в Cont. Потом
Проследим как это работает:
Всё как мы и ожидали. Рассмотрим случай посложнее:
Когда when сработает, он вернёт
Функции
Например:
Всё это сильно напоминает goto, правда здесь мы можем перемещаться только в прошлые состояния.
Функция
Попробуем разобраться. Начнём её упрощать:
Функция
А
Когда мы вызываем
Также, у
Программа будет спрашивать у пользователя разрешения на продолжение, пока он не ответит «n».
Отсюда видно, что
С помощью продолжений можно гибко управлять ходом программы, возвращаться из функции до её завершения, создать систему исключений.
Также возможно «приостановить» вычисление, возвращаясь к нему в другое время (например, Hugs использует продолжения для реализации параллелизма).
В основном, Cont используется вкупе с другими монадами, как трансформер. Это позволяет облегчить создание сложных управляющих конструкций и/или сделать вычисления более быстрыми.
Continuation passing style
Goto in Haskell
Making Haskell programs faster and smaller
С помощью продолжений можно реализовать обработку исключений, подобие goto и множество других вещей напоминающих императивные конструкции.
Также, используя продолжения можно улучшить производительность программы, убирая ненужные «обёртывания» и сопоставления с образцом.
В этой статье я расскажу, как можно реализовать продолжения в Haskell, и покажу несколько интересных функций работающих с ними.
Программирование в стиле продолжений
Для начала, посмотрим что такое продолжение и программирование в стиле продолжений.
Обычные функции:
square :: Int -> Int
square x = x*x
incr :: Int -> Int
incr x = x+1
func :: Int -> Int
func x = square (incr x)
А теперь в стиле продолжений:
square_cps :: Int -> (Int -> r) -> r
square_cps x k = k (x*x)
incr_cps :: Int -> (Int -> r) -> r
incr_cps x k = k (x+1)
func_cps :: Int -> (Int -> r) -> r
func_cps x k = incr_cps x $ \inc ->
square_cps inc $ \sq ->
k sq
Теперь функции кроме непосредственно аргументов принимают на вход функцию, которая будет применена к результату. Это и есть продолжение.
С помощью продолжений мы можем соединять функции, что и происходит в
func_cps
. Сначала выполняется incr_cps
, и её результат «попадает» в продолжение (\inc -> ...)
, потом вызывается square_cps
чей результат передаётся продолжению (\sq -> ...)
, который наконец отдаётся самому внешнему продолжению k
.Продолжения здесь имеют тип
(Int -> r)
так как не обязательно, что продолжение вернёт Int
. Например, чтобы вывести результат на консоль, мы можем передать
print
в качестве продолжения:main = func_cps 5 print
Монада Cont
Можно заметить некоторые закономерности в стиле продолжений.
К примеру, простые функции, такие как
square_cps
и incr_cps
, мы объявляли похожим образом:function ... = \k -> k (...)
А соединяли мы их так:
fun1 ... $ \r1 ->
fun2 ... $ \r2 -> ...
Всё это напоминает нам о монадах, первый пример похож на
return
, а второй на >>=
.Введём монаду Cont как:
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
Но почему
(a -> r) -> r
?Дело в том, что когда мы писали функции в стиле продолжений, каждая функция брала дополнительный параметр, продолжающий вычисление.
Если мы «заполним» функцию в стиле продолжений аргументами (до самого продолжения), её тип будет
(a -> r) -> r
, где a
— тип результата функции, если бы мы просто возвратили его, без передачи продолжению, а r
— тип результата, который вернёт продолжение:> :t square_cps 5
square_cps :: (Int -> r) -> r
Попробуем сделать Cont монадой.
instance Monad (Cont r) where
return n = Cont $ \k -> k n
...
return n
— это Cont который сразу применяет n к продолжению, которое он получил. m >>= f = Cont $ \k -> runCont m (\a -> runCont (f a) k)
m >>= f
— это Cont который запускает (runCont
просто «распаковывает» Cont, освобождая функцию) m
с продолжением (\a -> runCont (f a) k)
, которое может быть получит результат вычисления, и присвоит его a
(а может и не получит, ведь функция может проигнорировать продолжение). Потом, a
будет применено к f
, чтобы получить другой Cont, который, в свою очередь, будет запущен с самым внешним продолжением k
.Перепишем нашу программу с использованием монады Cont:
square_Cont :: Int -> Cont r Int
square_Cont x = return (x*x)
incr_Cont :: Int -> Cont r Int
incr_Cont x = return (x+1)
func_Cont :: Int -> Cont r Int
func_Cont x = do inc <- incr_Cont x
sq <- square_Cont inc
return sq
main = runCont (func_Cont 5) print
Теперь всё выглядит намного понятней.
Перейдём к функциям, работающим с Cont.
callCC
Начнём с простого примера:
square :: Int -> Cont r Int
square n = callCC $ \k -> k (n*n)
Что за бесполезная функция? Больше похоже будто это просто синоним для конструктора Cont.
Но всё не так просто. Посмотрим на тип
callCC
:callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a
По настоящему,
callCC
берёт функцию, которая принимает другую функцию, типа (a -> Cont r b)
, а возвращает Cont r a
. То есть, k (n*n)
в нашем примере имеет тип Cont r Int
.Для чего можно использовать
callCC
? Например, для быстрого выхода из функции:import Control.Monad (when)
hehe :: Int -> Cont r String
hehe n = callCC $ \exit -> do
let fac = product [1..n]
when (n > 7) $ exit "OVER 9000"
return $ show fac
main = do n <- fmap read getLine
runCont (hehe n) putStrLn
Опробуем нашу программу:
> main
3
6
> main
10
OVER 9000
Эта программа вычисляет факториал, и если он оказывается больше 9000, возвращает сообщение «OVER 9000», а если нет, то просто его значение.
Здесь мы использовали
exit
как return в императивных языках — он прервал вычисление и вывел другой результат.Также возможно использование вложенных callCC-блоков:
bar :: String -> Cont r String
bar s = do
callCC $ \exit1 -> do
let ws = words s
names = foldl (\a c -> a ++ ", " ++ c) (head ws) (tail ws)
r' <- callCC $ \exit2 -> do
when (null ws) $ exit1 "No people"
when (length ws == 1) $ exit2 "There is "
return "There are: "
return $ r' ++ names
main = do ns <- getLine
runCont (bar ns) putStrLn
Попробуем:
> main
No people
> main
Bob
There is Bob
> main
Bob Alice
There are: Bob, Alice
Если список имён пустой, то вызываем
exit1 "No people"
, «перепрыгивая» через внутренний блок.Если человек один, используем «There is », если их много, то «There are: ».
Заметьте, что когда вычисление доходит до return в callCC-блоке, то результат возвращается как обычно.
Так как же реализована функция
callCC
? Давайте посмотрим:callCC f = Cont $ \k -> runCont (f (\a -> Cont $ \_ -> k a)) k
Начинается она по обычному, с оборачивания функции в стиле продолжений в Cont. Потом
(f (\a -> Cont $ \_ -> k a))
запускается с продолжением k
.(\a -> Cont $ \_ -> k a)
— это функция, которая берёт что-то и возвращает Cont, игнорирующий своё продолжение и использующий k
взамен.Проследим как это работает:
square n = callCC $ \k -> k (n*n)
= Cont $ \k' -> runCont ((\k -> k (n*n)) (\a -> Cont $ \_ -> k' a)) k'
= Cont $ \k' -> runCont ((\a -> Cont $ \_ -> k' a) (n*n)) k'
= Cont $ \k' -> runCont (Cont $ \_ -> k' (n*n)) k'
= Cont $ \k' -> (\_ -> k' (n*n)) k'
= Cont $ \k' -> k' (n*n)
Всё как мы и ожидали. Рассмотрим случай посложнее:
hehe :: Int -> Cont r String
hehe n = callCC $ \exit -> do
let fac = product [1..n]
when (n > 7) $ exit "OVER 9000"
return $ show fac
= callCC $ \exit -> (when (n > 7) $ exit "OVER 9000") >> (return $ show (product [1..n])) -- Здесь я убрал let для простоты
= Cont $ \k -> runCont ((\exit -> (when (n > 7) $ exit "OVER 9000") >> (return $ show (product [1..n]))) (\a -> Cont $ \_ -> k a)) k
= Cont $ \k -> runCont ((when (n > 7) $ (\a -> Cont $ \_ -> k a) "OVER 9000") >> (return $ show (product [1..n]))) k
= Cont $ \k -> runCont ((when (n > 7) $ (Cont $ \_ -> k "OVER 9000")) >> (return $ show (product [1..n]))) k
Когда when сработает, он вернёт
(Cont $ \_ -> k "OVER 9000")
. Этот Cont не использует своё продолжение, значит и остальная часть кода не выполнится.getCC
Функции
getCC
и getCC'
позволяют нам «добыть» текущее продолжение и использовать его чтобы вернуться в предыдущее состояние программы.Например:
foo :: Int -> Cont r String
foo s = do (i, back) <- getCC' s
when (i < 20) $ back (i*2)
return $ show i
foo
удваивает свой аргумент пока он не станет больше или равным 20.> runCont (foo 5) id
"20"
> runCont (foo 3) id
"24"
> runCont (foo 2) id
"32"
(i, back) <- getCC' s
— присваивает i
значение s
и создаёт «ссылку» на это место.back (i*2)
— возвращается в прошлое состояние, но с i
равной i*2
.Всё это сильно напоминает goto, правда здесь мы можем перемещаться только в прошлые состояния.
Функция
getCC'
объявлена так:getCC' :: t -> Cont r (t, t -> Cont r a)
getCC' x0 = callCC (\c -> let f x = c (x, f) in return (x0, f))
Попробуем разобраться. Начнём её упрощать:
getCC' x0 = Cont $ \k -> runCont ((\c -> let f x = c (x, f) in return (x0, f)) (\a -> Cont $ \_ -> k a)) k
= Cont $ \k -> runCont (let f x = (\a -> Cont $ \_ -> k a) (x, f) in return (x0, f)) k
= Cont $ \k -> runCont (let f x = Cont $ \_ -> k (x, f) in return (x0, f)) k
= Cont $ \k -> runCont (let f x = Cont $ \_ -> k (x, f) in Cont $ \k' -> k' (x0, f)) k
= Cont $ \k -> let f x = Cont $ \_ -> k (x, f) in k (x0, f)
Функция
f
применяет пару из своего аргумента и себя к внешнему (getCC'-шному) продолжению, и оборачивает это в Cont. А
k (x0, f)
— применяет пару из аргумента getCC'
и f
к внешнему продолжению.Когда мы вызываем
f
в другом месте, он возвращает Cont, использующий не текущее продолжение, а то, которое было текущим для getCC'
. Таким образом, мы как-бы возвращаемся в прошлое состояние.Также, у
getCC'
есть «младший брат» — getCC
, но он полезен только с ContT (трансформер для Cont):import Control.Monad (when)
import Control.Monad.Cont
getCC :: MonadCont m => m (m a)
getCC = callCC (\c -> let x = c x in return x)
foo :: ContT () IO ()
foo = do back <- getCC
liftIO $ putStr "Do you want to coninue? [y/n] "
a <- liftIO $ getLine
when (a == "y" || a == "Y") $ back
main = runContT foo return
> main
Do you want to coninue? [y/n] y
Do you want to coninue? [y/n] y
Do you want to coninue? [y/n] n
>
Программа будет спрашивать у пользователя разрешения на продолжение, пока он не ответит «n».
Отсюда видно, что
getCC
только позволяет вернуться в прошлое состояние, но не даёт возможности передать аргументы.Где мне использовать Cont?
С помощью продолжений можно гибко управлять ходом программы, возвращаться из функции до её завершения, создать систему исключений.
Также возможно «приостановить» вычисление, возвращаясь к нему в другое время (например, Hugs использует продолжения для реализации параллелизма).
В основном, Cont используется вкупе с другими монадами, как трансформер. Это позволяет облегчить создание сложных управляющих конструкций и/или сделать вычисления более быстрыми.
Список используемых материалов
Continuation passing style
Goto in Haskell
Making Haskell programs faster and smaller