Русский вариант поста не исправляется, и возможно немного устарел, поэтому если вы видите явную ошибку, то гляньте английскую версию вернее всего там она исправлена. У меня просто нет времени на синхронизацию исправлений между двумя версиями.
В предыдущем посте мы описали, кратко, что такое монада и как мы ее можем представить в C#. Теперь давайте рассмотрим проблемы применения нашей реализации монад и опишем приемлемое решение. Итак, монады позволяют нам описывать некие абстрактные вычисления линия за линией и потом запускать их поверх контейнерных типов которые имплементируют наш IMonad интерфейс. Также в c# мы можем использовать синтаксический сахар, который позволяет нам выражать наши монадические вычисления в более приемлемом виде. По сути монада это компьютер, и наши абстрактные монадические вычисления это описание программы для него. Это определенно так, но мы можем задаться вопросом: являются ли наш монадический компьютер Тюринг полным? Можем ли мы выразить любой алгоритм на этом монадическом языке? И ответ да. Без этого свойства IO монада в Haskell не могла бы имитировать любой возможный путь исполнения с сайд эффектами. Но достаточно слов, давайте подтвердим это в коде. Давайте попробуем описать обычные конструкции для управление путем исполнения: “if else” and “while do” для нашего интерфейса IMonad и определим их в терминах методов bind и return.
Теперь мы можем описать любой поток исполнения для IMonad интерфейса. Давайте для примера расширим наш последний пример для монады Async < Check < > >.
Мы доказали, что мы можем описать любой алгоритм для наших монад. Но, к сожалению, этот код чрезвычайно тяжело читать и понимать. Было бы прекрасно, если бы мы могли использовать linq query для добавления синтаксического сахара. Давайте глянем на список возможных ключевых слов доступных для определения в linq queries: Where, Select, SelectMany, Join, GroupJoin, OrderBy, OrderByDescending, ThenBy, ThenByDescending, GroupBy, and Cast. Полный список методов, с сигнатурами типов, которые должны быть реализованы выглядит (так):
Как можно видеть, здесь нет ключевых слов для прямого представления конструкций ветвления и цикла, так как изначально linq queries были созданы для описания семантики языков которые похожи на языки реляционных или иерархических запросов. И это нормально, но не для описания семантики императивного языка.
Давайте суммируем наши наблюдения.
А что бы нам хотелось иметь?
К счастью для нас, мы уже имеем все из перечисленного в дот нет и это “computation expressions” языка fsharp. Что же это такое? Это простой класс построитель, в котором определены некоторые методы (наподобие SelectMany который мы определяли в linq). После этого мы можем использовать экземпляры этого класса, как имя для региона в котором мы можем использовать весь синтксис допустимый в “computation expressions” и этот синтаксис будет оттранслирован в вызовы методов этого экземпляра. Давайте посмотрим на простой пример.
Этот код взят из прекрасной серии постов “Computation Expressions”, я очень рекомендую вам прочитать материал целиком на сайте Fsharp for fun and profit. В “computation expressions” мы можем использовать синтаксис, который очень близок к fsharp, но имеет другую семантику определенную классом построителем. Мы можем использовать набор ключевых слов, наподобии let! с предыдущего примера, которые достаточны для выражения любого возможного пути исполнения императивной программы. Список возможных конструкций включает в себя циклы, try catch блоки, let do биндинги и многое другое. Все ключевые слова транслируются на вызовы соответствующих методов определенных в билдер классе, вот стандартный список методов: Bind, Delay, Return, ReturnFrom, Run, Combine, For, TryFinally,TryWith, Using, While, Yield, YieldFrom, Zero. Для примера, если мы определим метод While в нашем билдере, тогда мы сможем использовать циклы в регионе маркированным экземпляром этого билдера.
И мы можем передавать построители как обыкновенные значения, так как по сути они являются простыми обьектами.
И еде одно, мы можем определять не только новую семантику для существующих ключевых слов, но и добавлять новые ключевые слова. Например
“Computation expressions” в fsharp удовлетворяют все наши потребности из списка желаний. Теперь мы имеем возможность скрывать все нежелательные детали в билдере, а внутри вычислений писать абстрактный код который не содержит ненужных подробностей. Построители позволяют нам определять новые языки с необходимой семантикой и синтаксисом. Мы должны четко понимать, что “computation expressions” не просто монадический синтаксический сахар, наподобии do нотации в Haskell, а путь для описания новых языков внутри fsharp кода. Для примера Do нотация в Haskell часто заставляет программистов использовать монаду, вместо более простых конструкций наподобии моноидов(мы рассмотрим их в следующих постах), просто для того, чтобы иметь возможность использовать синтаксический сахар. Do нотация всегда Тюринг полня, но что если мы хотим построить небольшой язык который не является Тюринг полным? Для примера тотальный язык оторый имеет интересное свойство, что любая программа написанная на нем всегда гарантированно останавливается? Безопасные языки как правило не полны по Тюрингу. Do нотация этого не может, но “computation expressions” могут. “Computation expressions” имеют свои собственные проблемы. Они не полиморфны. Наш костыль с наследованием не будет работать в fsharp из за ограничений системы типов fsharp (F# не позволяет описывать ограничения генерик типов которые включают два разных параметра типа). Как результат, мы не можем выразить полиморфные монады и монад трансформеры в fsharp, но в общем случае это не является серьезной проблемой. В fsharp у нас нет необходимости так часто создавать композиции монад, как например в Haskell, где у нас есть IO монада практически везде и все неизменяемое. Но в случае необходимости можно построить специализированный построитель, который будет являться композицией двух других, обычно это не сверх сложная задача и он будет гораздо быстрее, чем идентичная ему монада построенная с помощью монад трансформера и у нас не будет проблем с сумасшедшими сигнатурами типов. Вы можете взглянуть на AsyncSeq и Update построители созданные Tomas Petricek-ом. Они являются хорошими примерами композиции построителей. “Computation expressions” это хороший путь для выражения языков ориентированных на определенную предметную область(internal DSL). Мы можем определять новый синтаксис и семантику. Однако отметим, что мы должны использовать операционную семантику(что ключевые слома должны делать), а не денотационную семантику(как ключевые слова должны быть переведены на другой язык). Если вам действительно необходимо выразить денотационную семантику, например как в Camlex.Net который транслирует C# в язык CAML запросов или FunScript который транслирует fsharp в JavaScript, тогда вам лучше использовать expression trees и другую фичу fsharp “code quotations”. Но вы также можете использовать построители вместе с expression trees или code quotations для выражения денатоцонной семантики вашего предметно ориентированного языка определенного в построителе, linq провайдеры именно так и работают. Для примера linq провайдеры для sql и xml определяют денотационную семантику запросов linq для трансляции их в sql и xpath. В fsharp есть QueryBuilder который работает точно также.