С# ждут продолжения: не совсем то же самое?

После прочтения ответа Эрика Липлетса У меня сложилось впечатление, что await и call/cc - это почти две стороны одной и той же монеты, с большинством синтаксических различий. Однако при попытке реально реализовать call/cc в С# 5 я столкнулся с проблемой: либо я неправильно понимаю call/cc (что довольно возможно), либо ждет только напоминание о вызове /cc.

Рассмотрим псевдокод следующим образом:

function main:
    foo();
    print "Done"

function foo:
    var result = call/cc(bar);
    print "Result: " + result;

function bar(continuation):
    print "Before"
    continuation("stuff");
    print "After"

Если мое понимание вызова /cc верное, тогда это должно печатать:

Before
Result: stuff
Done

Реально, когда вызывается продолжение, состояние программы восстанавливается вместе с историей вызова, так что foo возвращается в main и никогда не возвращается к bar.

Однако, если реализовано с помощью await в С#, вызов продолжения не позволяет восстановить эту историю вызовов. foo возвращается в bar, и не может (я могу видеть), что await может использоваться, чтобы сделать правильную историю вызовов частью продолжения.

Пожалуйста, объясните: я полностью неправильно понял операцию call/cc или await не совсем то же самое, что call/cc?


Теперь, когда я знаю ответ, я должен сказать, что это хорошая причина думать о них как о довольно похожей. Посмотрите, как выглядит вышеприведенная программа в псевдо-С# -5:

function main:
    foo();
    print "Done"

async function foo:
    var result = await(bar);
    print "Result: " + result;

async function bar():
    print "Before"
    return "stuff";
    print "After"

Итак, в то время как стиль С# 5 никогда не дает нам объект продолжения для передачи значения, общее сходство довольно впечатляет. За исключением того, что на этот раз совершенно очевидно, что "After" никогда не вызывается, в отличие от примера true-call/cc, что является еще одной причиной любить С# и хвалить его дизайн!

Ответ 1

await действительно не совсем то же самое, что call/cc.

Тип полностью фундаментального call/cc, о котором вы думаете, действительно должен был бы сохранить и восстановить весь стек вызовов. Но await - это просто преобразование времени компиляции. Он делает что-то подобное, но не использует реальный стек вызовов.

Представьте, что у вас есть функция async, содержащая выражение ожидания:

async Task<int> GetInt()
{
    var intermediate = await DoSomething();
    return calculation(intermediate);
}

Теперь представьте, что функция, которую вы вызываете через await, содержит выражение await:

async Task<int> DoSomething()
{
    var important = await DoSomethingImportant();
    return un(important);
}

Теперь подумайте, что произойдет, когда DoSomethingImportant() закончит и его результат будет доступен. Элемент управления возвращается к DoSomething(). Тогда DoSomething() заканчивается и что происходит тогда? Элемент управления возвращается к GetInt(). Поведение точно так же, как если бы GetInt() находился в стеке вызовов. Но это действительно не так; вы должны использовать await для каждого вызова, который вы хотите моделировать таким образом. Таким образом, стек вызовов поднимается в стек мета-вызовов, который реализуется в awaiter.

То же самое, кстати, верно для yield return:

IEnumerable<int> GetInts()
{
    foreach (var str in GetStrings())
        yield return computation(str);
}

IEnumerable<string> GetStrings()
{
    foreach (var stuff in GetStuffs())
        yield return computation(stuff);
}

Теперь, если я вызываю GetInts(), то, что я возвращаю, является объектом, который инкапсулирует текущее состояние выполнения GetInts() (так что вызов MoveNext() на нем возобновляет операцию, где он был остановлен). Этот объект сам содержит итератор, который выполняет итерацию через GetStrings() и вызывает MoveNext(). Таким образом, реальный стек вызовов заменяется иерархией объектов, которые каждый раз воссоздают правильный стек вызовов с помощью серии вызовов MoveNext() на следующем внутреннем объекте.