C # жду продолжения: не совсем то же самое? - PullRequest
12 голосов
/ 22 марта 2012

После прочтения ответа Эрика Липперта у меня сложилось впечатление, что await и call/cc в значительной степени являются двумя сторонами одной медали, с большинством синтаксических различий. Однако, пытаясь реализовать call/cc в C # 5, я столкнулся с проблемой: либо я неправильно понимаю call / cc (что вполне возможно), либо await только напоминает call / 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"

Если я правильно понимаю call / cc, то это должно вывести:

Before
Result: stuff
Done

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

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

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


Теперь, когда я знаю ответ, я должен сказать, что есть веская причина считать их довольно похожими. Посмотрите, как выглядит вышеприведенная программа в псевдо-C # -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"

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

1 Ответ

19 голосов
/ 22 марта 2012

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

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

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

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 на каждый вызов, который вы хотите смоделировать таким образом.Таким образом, стек вызовов поднимается в стек мета-вызовов, который реализован в ожидании.

То же самое, кстати, верно для 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() для , .Таким образом, стек вызовов real заменяется иерархией объектов, которые каждый раз воссоздают правильный стек вызовов посредством серии вызовов на MoveNext() следующего внутреннего объекта.

...