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

Прочитав ответ Эрика Липперта, у меня сложилось впечатление, что 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 ответ

Решение

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

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

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

Другие вопросы по тегам