Рассмотрим следующий пример в Haskell функции quux вместе с определениями монады продолжения и callCC.

instance Monad (Cont r) where
    return x = cont ($ x)
    s >>= f  = cont $ \c -> runCont s $ \x -> runCont (f x) c

callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a
callCC f = cont $ \h -> runCont (f (\a -> cont $ \_ -> h a)) h

quux :: Cont r Int
quux = callCC $ \k -> do
    let n = 5
    k n
    return 25

Насколько я понимаю этот пример. Блок do можно представить как

k n >>= \_ -> return 25 == 
cont $ \c -> runCont (k n) $ \x -> runCont ((\_ -> return 25) x) c

И мы можем видеть из определения k, которое есть \a -> cont $ \_ -> h a, что в приведенном выше примере \x -> runCont ((\_ -> return 25) x) c передается в аргумент, который игнорируется с подчеркиванием. В конечном итоге return 25 фактически «игнорируется», потому что аргумент подчеркивания никогда не используется, поэтому из-за ленивой оценки он никогда не оценивается.

Итак, насколько я могу судить, эта реализация callCC фундаментально сильно зависит от ленивых вычислений. Как это callCC сделать на строгом (неленивом) функциональном языке?

7
user782220 21 Дек 2013 в 08:07
В некоторых языках Scheme он реализован как примоп
 – 
Daniel Gratzer
21 Дек 2013 в 09:29

1 ответ

Лучший ответ

Нет. Эта реализация callcc не зависит от ленивого вычисления. Чтобы доказать это, я реализую это на строгом функциональном языке и покажу, что все, что находится после k n, вообще не выполняется.

Строгий функциональный язык, который я буду использовать, - это JavaScript. Поскольку JavaScript не имеет статической типизации, вам не нужно объявлять newtype. Поэтому мы начнем с определения функций return и >>= монады Cont в JavaScript. Мы будем называть эти функции unit и bind соответственно:

function unit(a) {
    return function (k) {
        return k(a);
    };
}

function bind(m, k) {
    return function (c) {
        return m(function (a) {
            return k(a)(c);
        });
    };
}

Затем мы определяем callcc следующим образом:

function callcc(f) {
    return function (c) {
        return f(function (a) {
            return function () {
                return c(a);
            };
        })(c);
    };
}

Теперь мы можем определить quux следующим образом:

var quux = callcc(function (k) {
    var n = 5;

    return bind(k(n), function () {
        alert("Hello World!");
        return unit(25);
    });
});

Обратите внимание, что я добавил alert во второй аргумент для bind, чтобы проверить, выполняется он или нет. Теперь, если вы вызовете quux(alert), он отобразит 5, но не будет отображать "Hello World!". Это доказывает, что второй аргумент bind никогда не выполнялся. Посмотрите сами демонстрацию.

Почему так происходит? Начнем в обратном направлении от quux(alert). По бета-сокращению это эквивалентно:

(function (k) {
    var n = 5;

    return bind(k(n), function () {
        alert("Hello World!");
        return unit(25);
    });
})(function (a) {
    return function () {
        alert(a);
    };
})(alert);

При уменьшении бета-версии это снова становится:

bind(function () {
    alert(5);
}, function () {
    alert("Hello World!");
    return unit(25);
})(alert);

Затем при бета-сокращении bind мы получаем:

(function (c) {
    return (function () {
        alert(5);
    })(function (a) {
        return (function () {
            alert("Hello World!");
            return unit(25);
        })(a)(c);
    });
})(alert);

Теперь мы можем понять, почему "Hello World!" никогда не отображался. Путем сокращения бета-версии мы выполняем function () { alert(5); }. Задача этой функции - вызвать свой аргумент, но этого не происходит. Из-за этого выполнение останавливается, и "Hello World!" никогда не отображается. В заключении:

Функция callcc не зависит от ленивого вычисления.

Функция, созданная callcc, завершается после вызова k не из-за ленивого вычисления, а из-за того, что вызов k разрывает цепочку, не вызывая его первый аргумент, и, следовательно, немедленно возвращается.

Это возвращает меня к вашему вопросу:

И мы можем видеть из определения k, которое есть \a -> cont $ \_ -> h a, что в приведенном выше примере \x -> runCont ((\_ -> return 25) x) c передается в аргумент, который игнорируется с подчеркиванием. В конечном итоге return 25 фактически «игнорируется», потому что аргумент подчеркивания никогда не используется, поэтому из-за ленивой оценки он никогда не оценивается.

Вы ошибаетесь. Как видите, k равно (\a -> cont $ \_ -> h a), а функция (\x -> runCont ((\_ -> return 25) x) c) передается в аргумент, который игнорируется k. До этого вы были правы. Однако это не означает, что return 25 не оценивается из-за ленивого вычисления. Он просто не оценивается, потому что функция (\x -> runCont ((\_ -> return 25) x) c) никогда не вызывается. Надеюсь, это прояснило ситуацию.

7
Aadit M Shah 21 Дек 2013 в 10:39
1
О, теперь я понимаю, что думал, что передача функции имеет какое-либо отношение к оценке функции, что, конечно же, вздор.
 – 
user782220
21 Дек 2013 в 10:51