Использование монадных преобразователей и продолжений для создания минимального интерпретатора для процедуры раннего возврата - PullRequest
2 голосов
/ 26 февраля 2020

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

data P = Px Int | Ps [P] | Pc P | Pr

Значение: Px x инструкция x, Ps xs последовательность инструкций, Pc x вызов подпроцедуры x, Pr досрочное возвращение. Конфигурация во время вычислений - это просто Int, а инструкция Px увеличивает ее (просто для визуализации этого минимального примера, я на самом деле применяю эту вещь к большему языку). Так, например, run 0 $ Ps [Px 1, Px 2] = [1,3] - это полный след выполнения, начиная с конфигурации 0.

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

runm :: Int -> P -> ([Int] -> Cont [Int] [Int]) -> Cont [Int] [Int]
runm c p k = case p of
  Px x -> return [c+x]
  Ps [] -> return []
  Ps (x:xs) -> do
    s <- runm c x k
    let c2 = if not (null s) then last s else c
        k' r = k $ s ++ r
    ss <- runm c2 (Ps xs) k'
    return $ s ++ ss
  Pc x -> callCC $ runm c x
  Pr -> k []

Кажется, что он работает правильно, например:

> evalCont $ runm 0 (Ps [Px 1, Pc $ Ps [Px 2, Pr, Px 100], Px 3]) undefined
[1,3,6]

Выполняет «1», «2» и «3», но правильно пропускает «100», как это происходит после возврата.

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

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

Будучи не очень знакомым с продолжениями и монадными трансформаторами, я все равно не добился большого успеха. Я даже не могу понять, в каком порядке мне строить стек монад.

Например:

runwc :: Int -> P -> ([Int] -> ContT [Int] (Writer [Int]) [Int]) -> ContT [Int] (Writer [Int]) [Int]
runwc c p k = case p of
  Px x -> (lift $ tell [c+x]) >> return []
  Ps [] -> return []
  Ps (x:xs) -> do
    (_,s) <- lift $ listen $ evalContT $ runwc c x k
    let c2 = if not (null s) then last s else c
    runwc c2 (Ps xs) k
  Pc x -> callCC $ runwc c x
  Pr -> k []

Это на самом деле не возвращает:

> execWriter $ evalContT $ runwc 0 (Ps [Px 1, Pc $ Ps [Px 2, Pr, Px 100], Px 3]) undefined
[1,3,103,106]

Я думаю, я не совсем понял, почему.

Противоположный порядок показался мне более многообещающим, но это тоже не работает:

runcw :: Int -> P -> (() -> WriterT [Int] (Cont [Int]) ()) -> WriterT [Int] (Cont [Int]) ()
runcw c p k = case p of
  Px x -> tell [c+x]
  Ps [] -> return ()
  Ps (x:xs) -> do
    (_,s) <- listen $ runcw c x k
    let c2 = if not (null s) then last s else c
    runcw c2 (Ps xs) k
  Pc x -> WriterT $ callCC $ \j -> runWriterT $ do
    let k' = \_ -> WriterT $ j ((), [])
    runcw c x k'
  Pr -> k ()

Возвращает слишком много:

> evalCont $ execWriterT $ runcw 0 (Ps [Px 1, Pc $ Ps [Px 2, Pr, Px 100], Px 3]) undefined
[1,4]

Разница в том, что здесь, я думаю, я понимаю это лучше: функция escaper j должна вызываться с собранной историей из последующего вызова runwc c x k', но она не получает ее. В j ((), []) все отбрасывается, а callCC возвращает пустой результат для Pc случая.

Моя идея состояла в том, что монада-писатель будет "независимо" собирать трассу, чтобы она присутствовала даже при прыжке через продолжения escaper, но, похоже, это не сработает, поскольку у j нет способа получить «прошлое» (я попробовал некоторую рекурсию из последующего вызова runcw, но это Loop).

Чтобы уточнить, что я ожидал сделать, я могу сделать это с помощью более мощной монады состояния:

runcs :: Int -> P -> (() -> StateT [Int] (Cont [Int]) ()) -> StateT [Int] (Cont [Int]) ()
runcs c p k = case p of
  Px x -> modify (++ [c+x])
  Ps [] -> return ()
  Ps (x:xs) -> do
    s <- runcs c x k >> get
    let c2 = if not (null s) then last s else c
    runcs c2 (Ps xs) k
  Pc x -> StateT $ \s -> callCC $ \j -> flip runStateT s $ do
    let k' = \_ -> StateT $ \s' -> j ((), s')
    runcs c x k'
  Pr -> k ()

Это работает правильно

> evalCont $ execStateT (runcs 0 (Ps [Px 1, Pc $ Ps [Px 2, Pr, Px 100], Px 3]) undefined) []
[1,3,6]

Это последнее решение освобождает меня от обработки продолжения escaper в случае последовательности Ps, и оно может указать - get, что произошло до возврата, чтобы выбросить его в j.

. проблема в том, что это, очевидно, слишком мощно: в любой момент можно получить доступ к полной трассе выполнения и манипулировать через «глобальное» состояние.

Можно ли получить преимущества State решение с использованием только Writer, так что каждый шаг интерпретатора может выполнять лишь небольшую часть добавления результата?

У меня сложилось впечатление, что правильная подпись на самом деле будет ContT r (Writer w) a даже если я получу больше прогресса с WriterT w (ContT r) a. Первый соответствует (a -> (r,w)) -> (r,w), а второй - ((a,w) -> r) -> r, и мне кажется, что этот последний несет в себе слишком большую симметрию между a и w, но здесь моя голова начала процедуру взрыва, и поэтому я спрашиваю, потому что это действительно первый раз, когда я делаю что-то значимое с продолжениями, кроме тривиальных тестов.

1 Ответ

3 голосов
/ 26 февраля 2020

Я закончил с этим, который, на мой взгляд, был значительно проще, чем оригинальный код, переместив все детали в стек monadi c. (Хорошо, если мы примем «сокрытие сложности» как «упрощение», по крайней мере. :-P)

import Control.Monad.Cont
import Control.Monad.Trans.RWS

data P = Px Int | Ps [P] | Pc P | Pr

-- A shorthand
type T = ContT () (RWS () [Int] Int) ()

runwc :: P -> (() -> T) -> T
runwc p k = case p of
  Px x -> lift $ do
     c <- get
     tell [c+x]
     put (c+x)
  Ps xs -> mapM_ (flip runwc k) xs
  -- Equivalent to:
  -- Ps [] -> return ()
  -- Ps (x:xs) -> do
  --   runwc x k
  --   runwc (Ps xs) k
  Pc x -> callCC $ runwc x
  Pr -> k ()

test :: [Int]
test = trace
   where
   (_result, _counter, trace) = runRWS action () 0
   action = runContT (runwc (Ps [Px 1, Pc $ Ps [Px 2, Pr, Px 100], Px 3]) return)
            (const $ return ())

Вывод будет

> test
[1,3,6]

, как и предполагалось.

Основной монади c type T нуждается в некотором комментарии:

type T = ContT () (RWS () [Int] Int) ()
            -- 1       2  3     4    5

Здесь:

  1. () - тип конечного результата После того, как продолжение было применено. Я выбрал (), поскольку трасса находится в монаде «писатель», но может быть иным.
  2. () - это тип состояния только для чтения (часть «reader» в RWS ). Это тривиально, так как нам это не нужно.
  3. [Int] тип трассы (часть "Writer" RWS).
  4. Int тип counter (часть "state" RWS).
  5. () тип результата действия monadi c, который будет передан в продолжение (может быть что-то еще) .

Остальная часть кода должна быть более или менее понятной. Px получает состояние, увеличивает его и регистрирует. Ps тривиально: мы вызываем runwc p k для каждого p в блоке. Pc устанавливает текущее продолжение. Pr вызывает продолжение набора.

...