Имеет ли монада IO смысл на языке, таком как C #

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

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

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

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

Итак, мой вопрос: справедливо ли говорить, что монада IO действительно полезна только в Haskell? Есть ли веская причина для внедрения монады IO, скажем, в C #?

Я регулярно использую Haskell и F #, и мне никогда не нравилось использование IO или государственной монады в F #.

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

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

Монады также не очень хорошо работают на платформе .NET из-за отсутствия более высоких типов : в то время как вы можете писать монадический код в F # с синтаксисом рабочего процесса, а на C # с болью больше, вы не можете легко писать код, который абстрагируется от нескольких разных монад.

На работе мы используем monads для управления IO в нашем коде C # на наших наиболее важных бизнес-логиках. Два примера – это наш финансовый код и код, который находит решения для проблемы оптимизации для наших клиентов.

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

interface IFinancialOperationVisitor : IMonadicActionVisitor { R GetTransactions(GetTransactions op); R PostTransaction(PostTransaction op); } interface IFinancialOperation { R Accept(IFinancialOperationVisitor visitor); } class GetTransactions : IFinancialOperation>> { Account Account {get; set;}; public R Accept(IFinancialOperationVisitor visitor) { return visitor.Accept(this); } } class PostTransaction : IFinancialOperation> { Transaction Transaction {get; set;}; public R Accept(IFinancialOperationVisitor visitor) { return visitor.Accept(this); } } 

который по существу является кодом Haskell

 data FinancialOperation a where GetTransactions :: Account -> FinancialOperation (Either Error [Transaction]) PostTransaction :: Transaction -> FinancialOperation (Either Error Unit) 

наряду с абстрактным синтаксическим деревом для построения действий в монаде, по существу свободной монады:

 interface IMonadicActionVisitor { R Return(T value); R Bind(IMonadicAction input, Func> projection); R Fail(Errors errors); } // Objects to remember the arguments, and pass them to the visitor, just like above /* Hopefully I got the variance right on everything for doing this without higher order types, which is how we used to do this. We now use higher order types in c#, more on that below. Here, to avoid a higher-order type, the AST for monadic actions is included by inheritance in */ 

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

В нашем коде оптимизации мы используем монаду для управления IO для получения внешних данных для оптимизации. Это позволяет нам писать код, который не знает, как составлены вычисления, что позволяет нам использовать точно такой же бизнес-код в нескольких настройках:

  • синхронный ИО и вычисления для вычислений, выполненных по требованию
  • asynchronous IO и вычисления для многих вычислений, выполненных параллельно
  • издевается над IO для модульных тестов

Поскольку код должен быть передан, какую монаду нужно использовать, нам нужно четкое определение монады. Вот он. IEncapsulated существу означает TClass . Это позволяет компилятору c # отслеживать все три части типа монад одновременно, преодолевая необходимость использования при работе с монадами.

 public interface IEncapsulated { TClass Class { get; } } public interface IFunctor where F : IFunctor { // Map IEncapsulated Select(IEncapsulated initial, Func projection); } public interface IApplicativeFunctor : IFunctor where F : IApplicativeFunctor { // Return / Pure IEncapsulated Return(A value); IEncapsulated Apply(IEncapsulated> projection, IEncapsulated initial); } public interface IMonad : IApplicativeFunctor where M : IMonad { // Bind IEncapsulated SelectMany(IEncapsulated initial, Func> binding); // Bind and project IEncapsulated SelectMany(IEncapsulated initial, Func> binding, Func projection); } public interface IMonadFail : IMonad { // Fail IEncapsulated Fail(TError error); } 

Теперь мы могли бы представить себе другой class монады для части IO, которую наши вычисления должны уметь видеть:

 public interface IMonadGetSomething : IMonadFail { IEncapsulated GetSomething(); } 

Затем мы можем написать код, который не знает, как собираются вычисления

 public class Computations { public IEncapsulated> GetSomethings(IMonadGetSomething monad, int number) { var result = monad.Return(Enumerable.Empty()); // Our developers might still like writing imperative code for (int i = 0; i < number; i++) { result = from existing in r1 from something in monad.GetSomething() select r1.Concat(new []{something}); } return result.Select(x => x.ToList()); } } 

Это можно использовать повторно как в синхронной, так и в асинхронной реализации IMonadGetSomething<> . Обратите внимание, что в этом коде GetSomething() s будет происходить один за другим, пока не появится ошибка, даже в асинхронной настройке. (Нет, это не то, как мы строим списки в реальной жизни)

Вы спрашиваете: «Нужна ли нам монада IO в C #?» но вы должны спросить: «Нужен ли нам способ надежно получить чистоту и неизменность в C #?».

Ключевым преимуществом будет контроль над побочными эффектами. Если вы делаете это, используя монады или какой-то другой механизм, это не имеет значения. Например, C # может позволить вам отмечать методы как pure а classы – immutable . Это отлично подойдет для приручения побочных эффектов.

В такой гипотетической версии C # вы попытались бы сделать 90% вычислений чистыми и иметь неограниченные, нетерпеливые IO и побочные эффекты в оставшихся 10%. В таком мире я не вижу столь необходимой потребности в абсолютной чистоте и монаде IO.

Обратите внимание, что просто механически преобразуя побочный код в монадический стиль, вы ничего не получаете. Код вообще не улучшает качество. Вы улучшаете качество кода за счет чистоты на 90% и концентрируете IO в небольших, легко просматриваемых местах.

Возможность узнать, имеет ли функция побочные эффекты, просто взглянув на ее подпись, очень полезна, когда вы пытаетесь понять, что делает функция. Чем меньше функция может делать, тем меньше вы должны понимать! (Полиморфизм – это еще одна вещь, которая помогает ограничить то, что функция может делать с ее аргументами.)

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

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

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

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

Другим преимуществом монады IO является то, что, поскольку действия ИО «инертны», если они не лежат на пути main функции, легко манипулировать ими как данными, помещать их в контейнеры, составлять их во время выполнения и т. Д.

Разумеется, у монадического подхода к IO есть свои недостатки. Но у этого есть преимущества, кроме «одного из немногих способов делать ввод-вывод на чистом ленивом языке гибким и принципиальным образом».

Как всегда, монада IO является особой и трудноразрешимой. В сообществе Haskell хорошо известно, что, хотя IO полезен, он не разделяет многих преимуществ других монадов. Использование, как вы заметили, сильно мотивировано позицией привилегий, а не хорошим инструментом моделирования.

С этим я бы сказал, что это не так полезно на C # или, действительно, на любом языке, который не пытается полностью содержать побочные эффекты с аннотациями типа.

Но это всего лишь одна монада. Как вы уже упоминали, в LINQ появляется Failure, но более сложные монады полезны даже на языке побочных эффектов.

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

Чтобы идти дальше, мой пример – представить что-то вроде монаршицы из Parser. Наличие этой монады, даже в C #, является отличным способом локализовать такие вещи, как отказ от детерминированного отказа, который выполняется при потреблении строки. Вы, очевидно, можете сделать это с определенными видами изменчивости, но Monads выражают, что конкретное выражение выполняет полезное действие в этом эффективном режиме, не обращая внимания ни на какое глобальное состояние, к которому вы также можете столкнуться.

Итак, я бы сказал, да, они полезны на любом типизированном языке. Но IO как Haskell это делает? Может быть, не так много.

На языке, таком как C #, где вы можете делать IO в любом месте, монада IO практически не имеет практического применения. Единственное, что вы хотели бы использовать для этого – это контролировать побочные эффекты, и поскольку нет ничего, что мешает вам выполнять побочные эффекты вне монады, на самом деле не так много смысла.

Что касается монады Maybe , хотя она кажется потенциально полезной, она действительно работает только на языке с ленивой оценкой. В следующем выражении Haskell второй lookup не оценивается, если первый возвращает Nothing :

 doSomething :: String -> Maybe Int doSomething name = do x <- lookup name mapA y <- lookup name mapB return (x+y) 

Это позволяет выражению «короткое замыкание», когда встречается « Nothing . Реализация в C # должна была бы выполнять оба поиска (я думаю, мне было бы интересно увидеть встречный пример.) Вероятно, вы более обеспечены операторами if.

Другой проблемой является потеря абстракции. В то время как, конечно, возможно реализовать монады в C # (или вещи, которые выглядят немного как монады), вы не можете на самом деле обобщать, как вы можете в Haskell, потому что C # не имеет более высоких видов. Например, такая функция, как mapM :: Monad m => Monad m => (a -> mb) -> [a] -> m [b] (которая работает для любой монады), на самом деле не может быть представлена ​​в C #. У вас могло бы быть что-то вроде этого:

 public List mapM(Func>); 

который будет работать для определенной монады ( Maybe в этом случае), но невозможно абстрагироваться от Maybe из этой функции. Вы должны были бы сделать что-то вроде этого:

 public List mapM(Func>); 

что невозможно в C #.