Асинхронное Программирование И Вычислительные Выражения

В предыдущих заметках ( часть I , Часть II ) про async/await в C# 5 я писал, что аналогичный подход реализован в таких языках, как Haskell, F# и Nemerle, но, в отличие от C#, эти языки поддерживают концепцию более высокого уровня, позволяющую реализовать асинхронные вычисления.

в стиле async/await как библиотеки, а не на уровне языка.

Забавно, что в Nemerle сама эта концепция реализована в виде библиотеки.

Имя этому понятию — монада.

Помимо асинхронных вычислений, монады позволяют реализовать и другие вкусности, такие как понимание списка, продолжение, превращение грязных функций в чистый блок, через который неявно перетаскивается состояние, и многие другие.

Некоторые монады реализуют такие «желания» программистов на C#, как например, создание коллекций или выход foreach и выход из лямбда-выражений.

Цель этой заметки — введение в асинхронное программирование и выражения вычислений в Nemerle, но она также может быть полезна тем, кто изучает F#, поскольку реализация асинхронного программирования в Nemerle была сделана с прицелом на F#.

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

Наверное, каждый, кто осознал, что такое монада, пишет статью о том, что такое монада.

Я не был исключением.

Постараюсь сделать описание максимально кратким, чтобы знающие запомнили, а другие смогли понять дальнейшее повествование.



Монады

Монада — это порождающий шаблон, поддержка которого встроена в язык.

Реализация этого шаблона основана на следующей паре: полиморфный тип

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

interface F<T> { .

}

и пара операций над ним

class M { static public F<T> Return<T>(T obj) { .

} static F<U> Bind<T,U>(F<T> obj, Func<T, F<U>> fun) { .

} }

Return позволяет «обернуть» любое значение в монаду, а Bind позволяет выполнять над ним преобразования.

Очень слабую аналогию можно провести со StringBuilder, которому мы передаем начальное значение в качестве параметра конструктора, а затем используем методы Append* для его изменения.

Если вы замените F на IEnumerable, подпись Bind будет похожа на подпись SelectMany из Linq. Это неудивительно, поскольку Linq, с большими оговорками, тоже является монадой.

Кстати, об этом интересно рассказал Барт Де Смет на PDC2010 с докладом «LINQ, Take Two — Realizing the LINQ to Everything Dream» ( связь ).

Если Linq — монада, то попробуем сформулировать простое выражение Linq:

var nums = Enumerable.Range(-2, 7); var sqrt = from n in nums where n >=0 select Math.Sqrt(n);

с помощью М-операций.

Сначала объявим М-операции:

static class MList { static public IEnumerable<T> Return<T>(T obj) { var list = new List<T>(); list.Add(obj); return list; } static public IEnumerable<U> Bind<T, U>(this IEnumerable<T> obj, Func<T, IEnumerable<U>> fun) { return obj.SelectMany(fun); } static public IEnumerable<T> Empty<T>() { return Enumerable.Empty<T>(); } }

И перепишите выражение Linq:

var nums = Enumerable.Range(-2, 7); var sqrt = nums .

Bind(n => n >= 0 ? MList.Return(n) : MList.Empty<int>()) .

Bind(n => MList.Return(Math.Sqrt(n)));

Получилось даже хуже, чем с Linq, но это связано с тем, что в C# не встроена поддержка монад. В случае с Nemerle этот код может быть таким, мы объявляем М-операции:

class MList { public Return[T](obj : T) : IEnumerable[T] { def data = List(); data.Add(obj); data } public Bind[T, U](obj : IEnumerable[T], f : T->IEnumerable[U]) : IEnumerable[U] { obj.SelectMany(f) } public Empty[T]() : IEnumerable[T] { Enumerable.Empty() } }

И перепишите выражение Linq:

def mlist = MList(); def nums = Enumerable.Range(-2, 7); def sqrt = comp mlist { defcomp n = nums; defcomp n = if (n >= 0) mlist.Return(n) else mlist.Empty(); return Math.Sqrt(n :> double); };

Во-первых, помните, что def в Nemerle — это неизменяемый аналог var в C#, if — тернарный оператор (?:), и для вызова конструктора не требуется new. Теперь к делу: оператор comp объявляет начало монадических вычислений, следующий параметр обеспечивает М-операции, а затем уже идут собственно вычисления.

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

Этот пример приведен в образовательных целях; ниже приведены примеры, которые очень сложно повторить обычным кодом.

Давайте разберемся, как это работает. defcomp — магический оператор, который «превращает» монаду (в данном случае типа IEnumerable[T]) в значение (типа T), а return, наоборот, преобразует значение в монаду.

На самом деле никакого волшебства нет, просто выражение

defcomp n = nums; .



расширяется компилятором до

mlist.Bind(nums, n => .

)



Выражения вычислений

Если бы мы говорили о языке Haskell, то на этом история о монадах могла бы закончиться.

А вот в случае с гибридными языками (функциональными/императивными) ситуация немного сложнее, поскольку в них есть управляющие конструкции, такие как условные операторы, циклы и выход. Чтобы понять, в чем заключается проблема, мы можем попытаться выразить монадические вычисления в терминах M операций, которые содержат цикл и внутри цикла оператор defcomp. Решение этой проблемы довольно простое; вам необходимо добавить в набор М-операций методы, преобразующие операторы ветвления и цикла, например, While будет иметь следующую сигнатуру:

public F<FakeVoid> While<T>(Func<bool> cond, Func<F<FakeVoid>> body)

И когда компилятор встречает цикл, тело которого содержит монадические операторы, он сначала преобразует тело цикла в цепочку Bind, поскольку Bind возвращает F , то эту цепочку можно обернуть в лямбду "() => body()", например Func > компилятор также оборачивает условие цикла в лямбду, а затем передает эти лямбды в М-операцию While. Каждая операция M должна возвращать монаду, но цикл ничего не возвращает, поэтому не существует значения, которое можно было бы обернуть монадой.

Для этих целей используется синглтон типа FakeVoid. Теперь мы можем дать неформальное описание выражения вычисления; это монада для императивных языков.

В случае а-ля Haskell компилятор переписывает только defcomp и return внутри монадических вычислений, как уже отмечалось в случае императивных языков, переписываются и управляющие конструкции; в таблице ниже перечислены все переписанные операторы:

defcomp расширяет монаду в значение, близкое по смыслу к присваиванию
коллкомп разворачивает монаду, используется, когда значение не важно
возвращаться оборачивает аргумент в монаду, используется в конце блока монадических вычислений, смысл близок к возврату из функции
возврат комп аргумент — монада, возвращает эту монаду как результат блока монадических вычислений, в отличие от return он не оборачивает ее заново
урожай оборачивает аргумент в монаду и выполняет действия, аналогичные возврату доходности
доходность аргумент - монада, выполняет действия, аналогичные возврату доходности, в отличие от доходности не переоборачивает аргумент
если, когда, если только, пока, делать, foreach, for, using, попробовать.

поймать.

наконец

монадические версии обычных управляющих конструкций
Еще несколько слов о провайдерах М-операций.

Официально они называются билдерами и при построении вычислительного выражения используется утиная типизация, то есть билдеры должны содержать методы, которые использует компилятор, но от билдера не требуется реализация какого-либо интерфейса.

Это решение позволяет частично реализовать построители, если вы не планируете использовать все возможности вычислительного выражения.

Кстати, этот подход мы уже использовали при создании построителя MList (поддерживаются только defcomp и return).

Другая причина, по которой интерфейсы не используются, заключается в том, что они налагают более строгие условия на подпись М-операций.

Требуется только совместимость типов между монадами и М-операциями.

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

Для тех, кто хочет написать свои собственные сборщики, лучший совет — изучить исходный код. Выражения вычислений .



Примеры стандартных конструкторов

Стандартные построители встроены в библиотеку вычислительных выражений, и вместо того, чтобы создавать их экземпляры и передавать comp в качестве параметра, вам просто нужно передать их имя в качестве параметра.



Список
Построитель списков поддерживает стандартные языковые конструкции управления, а также выходные данные и выходные данныекомп.

Использует list[T] (стандартный список в Nemerle) в качестве монады.

Этот сборщик интересен тем, что он реализует два давних «желания» программистов C#: выход из лямбда-выражения и выход из коллекций.

Для начала давайте посмотрим на Linq-аналог запроса из начала статьи:

def num = Enumerable.Range(-2, 7); def sqrt : list[double] = comp list { foreach(n in num) when(n >= 0) yield Math.Sqrt(n); }

Как вы можете видеть, построитель списков позволяет вам использовать выражения доходности, не требуя объявления для этого функции, а также его можно использовать вместо ссылки на объекты.

Я считаю, что этот код гораздо легче читать, чем эквивалентное выражение linq. Теперь рассмотрим еще одно «пожелание» — сбор урожая.

Вначале я объявляю локальную функцию, которая генерирует последовательность, а затем вызываю ее дважды и делаю доходность коллекций.



def upTo(n) { comp list { for(mutable i=0;i<n;i++) yield i } } def twice = comp list { repeat(2) yieldcomp upTo(3); } Console.WriteLine(twice); //[0, 1, 2, 0, 1, 2]

Мне пришлось написать собственный генератор, а не использовать «Enumerable.Range(0, 3)» из-за типов: yieldcomp ожидает на входе монаду, ее тип в данном случае — list[int] и «Enumerable.Range(0) , 3)» « возвращает IEnumerable[int].

Чтобы преодолеть это несоответствие, существует еще один построитель — enumerable.

перечислимый
Этот построитель во многом аналогичен построителю списка, только он использует IEnumerable[T] в качестве типа монады и позволяет создавать бесконечные последовательности.

Перепишем последний пример:

def twice = comp enumerable { repeat(2) yieldcomp Enumerable.Range(0, 3); } foreach(item in twice) Console.Write($"$item "); //0, 1, 2, 0, 1, 2



Множество
Array ведет себя аналогично list и enumerable, но использует array[T] в качестве типа монады.



Асинхронный
Самый сложный, но очень полезный конструктор, во многом напоминающий будущий async/await в C#.

Он используется для создания асинхронных вычислений путем объединения существующих асинхронных вычислений.

Он поддерживает все операции, кроме доходности и доходности.

Тип монады этого построителя — Async[T].

Объекты этого типа описывают асинхронное вычисление, результатом которого будет значение типа T (аналог Task в С#); если асинхронная операция не возвращает значение, то вместо T используется конкретный тип FakeVoid. Операция Bind, ее тип Async[T]*(T-> Async[U])-> Async[U], «продолжается » асинхронного вычисления (тип Async[T]) с помощью функции, эта функция принимает объект типа T в качестве входных данных (результат асинхронной оценки) и возвращает новую асинхронную оценку типа Async[U].

Другой тип ключа — абстрактный класс ExecutionContext, экземпляры его потомков отвечают за выполнение асинхронной операции (например, в текущем потоке, в потоке из ThreadPool или с использованием SynchronizationContext), вот его сигнатура:

public abstract class ExecutionContext { public abstract Execute(computatuion : void -> void) : void; }

Чтобы запустить асинхронную операцию, необходимо вызвать метод Start объекта, описывающего асинхронную операцию (класс Async[T]), передав ему объект типа ExecutionContext; если метод вызывается без аргументов, то асинхронная операция запускается с помощью ThreadPool.QueueUserWorkItem. Расширение (async CTP), которое позволяет использовать реализацию async/wait в C#, уже поставляется со многими методами расширения, которые дополняют существующие классы асинхронными операциями.

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

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

public class HttpWebRequest : WebRequest, ISerializable { public override IAsyncResult BeginGetResponse(AsyncCallback callback, object state); public override WebResponse EndGetResponse(IAsyncResult asyncResult); }

Теперь давайте создадим асинхронное расширение, подходящее для использования в монадических вычислениях с использованием этих примитивов.



public module AsyncExtentions { public GetResponseAsync(this request : HttpWebRequest) : Async[WebResponse] { Async.FromBeginEnd(request.BeginGetResponse(_, null), request.EndGetResponse(_)) } }

Следует напомнить, что _ — специальный символ в Nemerle, в данном случае через него используется каррирование (обозначение f(_) эквивалентно x => f(x)).

Подобным образом можно создавать обёртки для любых стандартных асинхронных вычислений.

Попробуем написать что-нибудь из (C# 101) Async-примеров в Nemerle, например, параллельную загрузку нескольких веб-страниц и печать заголовка, код расширения GetHtml() и GetTitle() я опустил, статья и так длинная.



public PrintTitles() : Async[FakeVoid] { comp async { def response1 = HttpWebRequest.Create(" http://www.ya.ru ").

GetResponseAsync(); def response2 = HttpWebRequest.Create(" http://www.habr.ru ").

GetResponseAsync(); defcomp response1 = response1; defcomp response2 = response2; Console.WriteLine(response1.GetHtml().

GetTitle()); Console.WriteLine(response2.GetHtml().

GetTitle()); } }

Первые две строки запускают операции асинхронной загрузки страниц; эти методы возвращают объекты, описывающие асинхронные операции во время выполнения; с точки зрения компилятора тип этих объектов — Async[WebResponce] (монада).

В следующих двух строках монада раскрывается в значении; на другом уровне значения это означает ожидание результатов.

В последних строках обрабатываются результаты.

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

Но вернемся к нашему примеру.

Главное помнить, что монада — это генерирующий шаблон, вы создали функцию, которая описывает асинхронное вычисление, но не запускает его; вы можете запустить его так: PrintTitles().

Start().

GetResult().

На самом деле это очень важно, потому что это может быть источником ошибки, если метод возвращает Async[T], вам нужно знать, выполняет ли код вычисления или просто их создает. .

Чтобы различать, вероятно, стоит использовать соглашение об именах, например, методы, выполняющие вычисления, должны иметь суффикс Async. Во второй статье про async/await в C# я писал, что await запускает обработку результатов асинхронных вычислений в SynchronizationContext потока, запустившего асинхронные вычисления.

Nemerle обеспечивает большую гибкость в этом отношении; возможен перенос вычислений из одного потока в другой.

Рассмотрим обработчик нажатия кнопки:

private button1_Click (sender : object, e : System.EventArgs) : void { def formContext = SystemExecutionContexts.FromCurrentSynchronizationContext(); def task = comp async { Thread.Sleep(5000); callcomp Async.SwitchTo(formContext); label1.Text = "success"; } _ = task.Start(SystemExecutionContexts.ThreadPool()); }

Вначале мы получаем ExecutionContext, который запускает вычисления в текущем SynchronizationContext, затем описываем асинхронную операцию: Thread.Sleep эмулирует тяжелые вычисления, переключает контекст выполнения на контекст выполнения gui-потока и печатает результат. Сам расчет мы запускаем в ExecutionContexts пула потоков.

Это похоже на волшебство, но на самом деле всё это уже сделано, callcomp просто расширяет монаду, когда её значение не важно.

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

Помимо Async.SwitchTo есть и другие интересные монады, влияющие на поток выполнения, например, Async.Yield говорит, что контекст выполнения изменился, хотя и не меняет его.

В некоторых случаях это ничего не дает; в случае использования ThreadPool это действие вызывает переход к другому потоку из пула.



Заключение

В заключение могу лишь отметить, что монады — очень богатая тема.

В этой статье я не затронул такие классические монады, как State, Cont (продолжение), Maybe (еще одно «пожелание» фаната C#).

О них вы можете прочитать в других статьях, я постарался дать практическое объяснение, благодаря которому вы сможете начать использовать асинхронное программирование и списочные/перечислимые монады в Nemerle и знать, что происходит под капотом.

Во многом реализация await/async в будущем C# и монадический подход к асинхронному программированию в Nemerle схожи, но есть одно замечание: для поддержки await/async необходима следующая версия языка, причем асинхронная.

программирование в Nemerle реализуется посредством монад, которые, в свою очередь, являются частью стандартной библиотеки языка, а не языка).

Буду рад получить комментарии и ответить на вопросы.

Теги: #nemerle #монады #программирование

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.