Подтвердить что ты не робот

Как С# async/await относится к более общим конструкциям, например. F # workflows или monads?

Дизайн языка С# всегда (исторически) был направлен на решение конкретных проблем, а затем на поиск для решения основных проблем: см. http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/iterator-blocks-part-one.aspx для "IEnumerable vs. coroutines":

Мы могли бы сделать это гораздо более общим. Наши итераторные блоки можно рассматривать как слабый вид сопрограммы. Мы могли бы выбрали реализацию полных сопрограмм и просто сделали блок итераторов специальным случаем сопрограмм. И, конечно, сопрограммы, в свою очередь, менее общие, чем первоклассные продолжения; мы могли бы реализовывать продолжения, реализовывать сопрограммы с точки зрения продолжений и итераторы с точки зрения сопрограмм.

или http://blogs.msdn.com/b/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx для SelectMany как суррогат для (некоторых) Монадов:

Система типа С# недостаточно эффективна для создания обобщенной абстракции для монад, которая была основным мотиватором для создания методов расширения и "шаблона запроса"

Я не хочу спрашивать, почему так было (много хороших ответов уже даны, особенно в блоге Eric, которые могут применяться ко всем этим проектным решениям: от производительности до повышенной сложности, как для компилятора, так и для программиста).

То, что я пытаюсь понять, - это то, к чему относится "общая конструкция", к которой относятся ключевые слова async/await (моя лучшая догадка - продолжение монады - в конце концов, асинхронная реализация F # реализована с использованием рабочих процессов, что, на мой взгляд, является продолжением монады), и как они относятся к нему (как они отличаются друг от друга, чего нет?), почему есть пробел, если таковые имеются?)

Я ищу ответ, похожий на статью, связанную с Eric Lippert, связанную с ней, но связанную с async/await вместо IEnumerable/yield.

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

4b9b3361

Ответ 1

Модель асинхронного программирования в С# очень похожа на асинхронные рабочие процессы в F #, которые являются экземпляром общего шаблона монады. Фактически, синтаксис С# -тератора также является экземпляром этого шаблона, хотя ему нужна дополнительная структура, поэтому это не просто простая монада.

Объяснение этого далеко выходит за рамки одного ответа SO, но позвольте мне объяснить основные идеи.

Монадические операции. Асинхронный С# по существу состоит из двух примитивных операций. Вы можете await асинхронное вычисление, и вы можете return получить результат от асинхронного вычисления (в первом случае это делается с использованием нового ключевого слова, а во втором случае мы повторно используем ключевое слово, которое уже на языке).

Если вы выполняете общий шаблон (монада), вы переводите асинхронный код в вызовы следующих двух операций:

Task<R> Bind<T, R>(Task<T> computation, Func<T, Task<R>> continuation);
Task<T> Return<T>(T value);

Они могут быть легко реализованы с использованием стандартного API задачи - первая из них представляет собой комбинацию ContinueWith и Unwrap, а вторая просто создает задачу, которая немедленно возвращает значение. Я буду использовать эти две операции, потому что они лучше поймут идею.

Перевод. Главное - перевести асинхронный код в обычный код, который использует вышеуказанные операции.

Посмотрим на случай, когда мы получаем выражение e, а затем присваиваем результат переменной x и оцениваем выражение (или операторный блок) body (в С# вы можете ожидать внутри выражения, но вы всегда может перевести это в код, который сначала присваивает результат переменной):

[| var x = await e; body |] 
   = Bind(e, x => [| body |])

Я использую нотацию, которая довольно распространена в языках программирования. Смысл [| e |] = (...) заключается в том, что мы переводим выражение e (в "семантические скобки" ) на другое выражение (...).

В приведенном выше случае, когда у вас есть выражение с await e, оно переводится в операцию Bind, а тело (остальная часть ожидаемого кода) вставляется в функцию лямбда, которая передается как второй параметр Bind.

Вот что происходит! Вместо того, чтобы немедленно оценивать остальную часть кода (или блокировать поток во время ожидания), операция Bind может выполнять асинхронную операцию (представленную e, которая имеет тип Task<T>), и, когда операция завершается, может, наконец, вызвать функцию лямбда (продолжение), чтобы запустить остальную часть тела.

Идея перевода состоит в том, что он превращает обычный код, возвращающий некоторый тип R в задачу, которая возвращает значение асинхронно - это Task<R>. В приведенном выше уравнении тип возврата Bind является, действительно, задачей. Вот почему нам нужно перевести return:

[| return e |]
   = Return(e)

Это довольно просто - когда вы получаете результирующее значение и хотите его вернуть, вы просто завершаете его в задачу, которая немедленно завершается. Это может показаться бесполезным, но помните, что нам нужно вернуть Task, потому что операция Bind (и весь наш перевод) требует этого.

Более подробный пример. Если вы посмотрите на более крупный пример, содержащий несколько await s:

var x = await AsyncOperation();
return await x.AnotherAsyncOperation();

Код будет переведен на следующее:

Bind(AsyncOperation(), x =>
  Bind(x.AnotherAsyncOperation(), temp =>
    Return(temp));

Основной трюк заключается в том, что каждый Bind превращает остальную часть кода в продолжение (что означает, что он может быть оценен при завершении асинхронной операции).

Монады продолжения. В С# механизм async фактически не реализуется с использованием вышеуказанного перевода. Причина в том, что если вы фокусируетесь только на async, вы можете сделать более эффективную компиляцию (что и делает С#) и создать конечный автомат напрямую. Тем не менее, это в значительной степени связано с тем, как асинхронные рабочие процессы работают в F #. Это также является источником дополнительной гибкости в F # - вы можете определить свои собственные Bind и return для обозначения других вещей, таких как операции для работы с последовательностями, ведение журнала отслеживания, создание возобновляемых вычислений или даже объединение асинхронных вычислений с последовательностями ( асинхронная последовательность может давать несколько результатов, но может также ждать).

Реализация F # основана на продолжении монады, что означает, что Task<T> (фактически, Async<T>) в F # определяется примерно так:

Async<T> = Action<Action<T>> 

То есть, асинхронное вычисление - это какое-то действие. Когда вы дадите ему Action<T> (продолжение) в качестве аргумента, он начнет выполнять некоторую работу, а затем, когда он в конце концов закончит, он вызывает указанное вами действие. Если вы ищете продолжения монады, то я уверен, что вы можете найти лучшее объяснение этого как в С#, так и в F #, поэтому я остановлюсь здесь...

Ответ 2

Ответ Томаса очень хорош. Чтобы добавить еще несколько вещей:

Дизайн языка С# всегда (исторически) был направлен на решение конкретных проблем, а затем на поиск для решения основных проблем

Хотя в этом есть доля правды, я не думаю, что это вполне справедливая или точная характеристика, поэтому я собираюсь начать свой ответ, отрицая предпосылку вашего вопроса.

Конечно, верно, что на одном конце есть спектр "очень специфический" и "очень общий", а решения конкретных проблем относятся к этому спектру. С# разработан в целом как очень общее решение для множества конкретных проблем; что язык программирования общего назначения. Вы можете использовать С# для написания всего от веб-сервисов до игр XBOX 360.

Поскольку С# разработан как язык программирования общего назначения, когда команда разработчиков идентифицирует конкретную проблему пользователя, они всегда рассматривают более общий случай. LINQ - отличный пример. В самые ранние дни разработки LINQ это был не более чем способ поставить SQL-запросы в программу С#, поскольку это пространство проблем, которое было идентифицировано. Но довольно скоро в процессе проектирования команда поняла, что концепции сортировки, фильтрации, группировки и объединения данных применяются не только к табличным данным в реляционной базе данных, но также к иерархическим данным в XML и к специальным объектам в памяти. И поэтому они решили пойти на гораздо более общее решение, которое у нас есть сегодня.

Уловкой дизайна является выяснение, где по спектру имеет смысл остановиться. Команда дизайнеров могла бы сказать, что проблема понимания запросов на самом деле является лишь конкретным случаем более общей проблемы привязки монад. И проблема привязки монадов на самом деле является лишь конкретным случаем более общей проблемы определения операций над более высокими типами типов. И, конечно же, есть некоторая абстракция над системами типов... и достаточно. К тому времени, как мы решаем задачу bind-a-произвольной-монады, решение стало настолько общим, что программисты SQL-бизнес-класса, которые были мотивацией для этой функции в первую очередь, полностью потеряны, и мы убеждены Фактически они решили свою проблему.

Действительно основные функции, добавленные после того, как С# 1.0 - общие типы, анонимные функции, блоки итераторов, LINQ, dynamic, async - все они обладают свойством, что они являются очень общими функциями, полезными во многих разных доменах. Все они могут рассматриваться как конкретные примеры более общей проблемы, но это верно для любого решения любой проблемы; вы всегда можете сделать его более общим. Идея дизайна каждой из этих функций заключается в том, чтобы найти то место, где их нельзя сделать более общим, не запутывая своих пользователей.

Теперь, когда я отрицал предпосылку вашего вопроса, давайте посмотрим на фактический вопрос:

То, что я пытаюсь понять, - это то, что "общая конструкция", ключевые слова async/await относятся к

Это зависит от того, как вы на это смотрите.

Функция асинхронного ожидания построена вокруг типа Task<T>, который, как вы отмечаете, является монадой. И, конечно, если бы вы говорили об этом с Эриком Мейджером, он сразу же отметил бы, что Task<T> на самом деле является comonad; вы можете получить значение T обратно на другом конце.

Еще один способ взглянуть на эту функцию - взять абзац, который вы цитировали об итераторных блоках, и заменить "асинхронный" на "итератор". Асинхронные методы, подобно методам итератора, являются своего рода сопрограммой. Вы можете придумать Task<T> как только деталь реализации механизма сопрограммы, если хотите.

Третий способ взглянуть на эту функцию - сказать, что это своего рода вызов с продолжением (обычно сокращенный вызов /cc ). Это не полная реализация call/cc, поскольку она не принимает состояние стека вызовов в то время, когда продолжение подписывается во внимание. См. Этот вопрос для деталей:

Как можно реализовать новую функцию async в С# 5.0 с помощью вызова /cc?

Я подожду и посмотрю, сможет ли кто-нибудь (Эрик? Джон? может быть, вы?) заполнить более подробную информацию о том, как на самом деле С# генерирует код для ожидания,

Переписывание - это, по сути, просто вариация того, как перезаписываются блоки итератора. Мэдс просматривает все подробности в своей статье журнала MSDN:

http://msdn.microsoft.com/en-us/magazine/hh456403.aspx