Пример исполняется в монаде ContT() IO
, Монаде разрешает продолжение, которое приводит к ()
, а некоторые подняты IO
.
type ExM a = ContT() IO a
ContT
может быть невероятно запутанная монадой работать, но я обнаружил, что Эквациональная рассуждения Haskell является мощным инструментом для распутывания его. Остальная часть этого ответа исследует оригинальный пример в несколько этапов, каждый из которых снабжен синтаксическими преобразованиями и чистыми переименованиями.
Итак, давайте сначала рассмотрим тип части callCC
- это, в конечном счете, сердце всей этой части кода. Этот кусок отвечает за создание странного типа кортежа в качестве его монадического значения.
type ContAndPrev = (Int -> ExM(), Int)
getContAndPrev :: ExM ContAndPrev
getContAndPrev = callCC $ \k -> let f x = k (f, x)
in return (f, 0)
Это может быть немного больше знакомы секционированиями его с (>>=)
, что именно то, как он будет использоваться в реальном контекстуальном любом do
-блока desugaring создаст (>>=)
для нас в конце концов.
withContAndPrev :: (ContAndPrev -> ExM()) -> ExM()
withContAndPrev go = getContAndPrev >>= go
и, наконец, мы можем проверить, что на самом деле это выглядит на сайте вызова. Чтобы быть более ясным, я немного забуду исходный пример
flip runContT return $ do
lift (putStrLn "alpha")
withContAndPrev $ \(k, num) -> do
lift $ putStrLn "beta"
lift $ putStrLn "gamma"
if num < 5
then k (num + 1) >> return()
else lift $ print num
Обратите внимание, что это чисто синтаксическое преобразование. Код идентичен исходному примеру, но он подчеркивает существование этого отступованного блока под withContAndPrev
. Это секрет понимания Haskell callCC
--- withContAndPrev
предоставляется доступ ко всему «остатку блока do
», который он получает, чтобы выбрать, как использовать.
Давайте проигнорируем фактическую реализацию withContAndPrev
и просто посмотрим, можем ли мы создать поведение, которое мы видели при запуске примера. Это довольно сложно, но то, что мы хотим сделать, это передать в блок возможность называть себя. Хаскелл был ленив и рекурсивным, мы можем написать это напрямую.
withContAndPrev' :: (ContAndPrev -> ExM()) -> ExM()
withContAndPrev' = go 0 where
go n next = next (\i -> go i next, n)
Это все еще что-то вроде рекурсивной головной боли, но было бы легче увидеть, как это работает. Мы берем оставшуюся часть блока do и создаем петлевую конструкцию под названием go
. Мы передаем в блок функцию, которая вызывает наш looper, go
, с новым целочисленным аргументом и возвращает предыдущий.
Мы можем немного развернуть этот код, сделав еще несколько синтаксических изменений исходного кода.
maybeCont :: ContAndPrev -> ExM()
maybeCont k n | n < 5 = k (num + 1)
| otherwise = lift (print n)
bg :: ExM()
bg = lift $ putStrLn "beta" >> putStrLn "gamma"
flip runContT return $ do
lift (putStrLn "alpha")
withContAndPrev' $ \(k, num) -> bg >> maybeCont k num
И теперь мы можем рассмотреть, как это выглядит, когда betaGam >> maybeCont k num
получает передается в withContAndPrev
.
let go n next = next (\i -> go i next, n)
next = \(k, num) -> bg >> maybeCont k num
in
go 0 next
(\(k, num) -> betaGam >> maybeCont k num) (\i -> go i next, 0)
bg >> maybeCont (\i -> go i next) 0
bg >> (\(k, num) -> betaGam >> maybeCont k num) (\i -> go i next, 1)
bg >> bg >> maybeCont (\i -> go i next) 1
bg >> bg >> (\(k, num) -> betaGam >> maybeCont k num) (\i -> go i next, 2)
bg >> bg >> bg >> maybeCont (\i -> go i next) 2
bg >> bg >> bg >> bg >> maybeCont (\i -> go i next) 3
bg >> bg >> bg >> bg >> bg >> maybeCont (\i -> go i next) 4
bg >> bg >> bg >> bg >> bg >> bg >> maybeCont (\i -> go i next) 5
bg >> bg >> bg >> bg >> bg >> bg >> lift (print 5)
Таким образом, наша фальшивая реализация воссоздает поведение исходного цикла. Может быть немного более ясно, как наше поддельное поведение достигает этого, связывая рекурсивный узел, используя «остаток блока do», который он получает в качестве аргумента.
Вооруженные этими знаниями, мы можем подробнее рассмотреть callCC
. Мы снова получим прибыль, предварительно изучив его в своей предварительной форме. Это очень просто, если странно, в этой форме.
withCC gen block = callCC gen >>= block
withCC gen block = block (gen block)
Другими словами, мы используем аргумент callCC
, gen
, чтобы сформировать возвращаемое значение callCC
, но мы переходим в gen
само продолжение block
, что мы в конечном итоге применяя значение. Он рекурсивно триппирован, но denotationally clear - callCC
действительно «вызывает этот блок с текущим продолжением».
withCC (\k -> let f x = k (f, x)
in return (f, 0)) next
next (let f x = next (f, x) in return (f, 0))
Фактические детали реализации callCC
немного более сложной задачей, так как они требуют, чтобы мы нашли способ определения callCC
из семантики (callCC >>=)
, но это в основном игнорируемые. В конце дня мы получаем прибыль от того, что блоки do
записываются так, что каждая строка получает оставшуюся часть связанного с ним блока с (>>=)
, что обеспечивает естественное представление о продолжении немедленно.
Вы также можете создать 'newtype', чтобы обернуть бесконечный (equi-) рекурсивный тип в (iso-) рекурсивный' newtype'. – augustss
@augustss Это очень верно, я просто сконцентрировался больше на вопросе о том, почему пример, который они видели, использовал привязку let в callCC lambda.В некотором смысле использование привязки let может считаться лучше или хуже, поскольку это заставляет рекурсию выполняться с тем же продолжением, тогда как с помощью подхода newtype вы могли бы смешивать различные продолжения (это может быть плохо или хорошо хотя, я думаю). – DarkOtter
Продолжения в целом - очень острое оружие, которое можно использовать для хорошего и плохого. В основном плохо. :) – augustss