Помогите разработчику С# понять: что такое монада?

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

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

Однако, конечно, можно передать концепцию? По крайней мере, я надеюсь. Возможно, вы можете представить пример С# в качестве основы, а затем описать, что разработчик С# мог пожелать, он мог бы сделать оттуда, но не может, потому что язык не имеет функций функционального программирования. Это было бы фантастически, потому что это передало бы намерения и преимущества монадов. Итак, вот мой вопрос: Какое лучшее объяснение вы можете дать монадам разработчику С# 3?

Спасибо!

(EDIT: Кстати, я знаю, что на SO есть как минимум 3 вопроса "что такое монада". Тем не менее, я сталкиваюсь с одной и той же проблемой с ними... так что этот вопрос нужен imo из-за С# -разработчик. Спасибо.)

Ответы

Ответ 1

Большая часть того, что вы делаете в программировании весь день, объединяет некоторые функции вместе, чтобы создавать из них большие функции. Обычно у вас есть не только функции в панели инструментов, но и другие вещи, такие как операторы, назначения переменных и т.п., Но обычно ваша программа объединяет множество "вычислений" в более крупные вычисления, которые будут объединены вместе.

Монада - это способ сделать это "объединение вычислений".

Обычно ваш самый основной "оператор" для объединения двух вычислений - ;:

a; b

Когда вы говорите это, вы имеете в виду "сначала сделайте a, затем выполните b". Результат a; b - это в основном снова вычисление, которое можно объединить вместе с большим количеством материала. Это простая монада, это способ расчесывания небольших вычислений для более крупных. ; говорит: "Делайте то, что слева, а затем делайте что-то справа".

Еще одна вещь, которая может рассматриваться как монада в объектно-ориентированных языках, - это .. Часто вы найдете такие вещи:

a.b().c().d()

. в основном означает "оценить вычисление слева, а затем вызвать метод справа от результата". Это еще один способ объединить функции/вычисления вместе, немного сложнее, чем ;. И концепция объединения вещей вместе с . является монадой, поскольку она способ сочетания двух вычислений вместе с новым вычислением.

Другая довольно распространенная монада, у которой нет специального синтаксиса, есть этот шаблон:

rv = socket.bind(address, port);
if (rv == -1)
  return -1;

rv = socket.connect(...);
if (rv == -1)
  return -1;

rv = socket.send(...);
if (rv == -1)
  return -1;

Возвращаемое значение -1 указывает на сбой, но нет реального способа абстрагироваться от этой проверки ошибок, даже если у вас много API-вызовов, которые вам нужно комбинировать таким образом. Это, по сути, просто еще одна монада, которая объединяет вызовы функций по правилу "если функция слева вернула -1, верните сами -1, иначе вызовите функцию справа". Если бы у нас был оператор >>=, который сделал эту вещь, мы могли бы просто написать:

socket.bind(...) >>= socket.connect(...) >>= socket.send(...)

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

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

Например, приведенный выше >>= может быть расширен, чтобы "выполнить проверку ошибок, а затем вызвать правую сторону в сокете, который мы получили как входной", так что нам не нужно явно указывать socket много раз:

new socket() >>= bind(...) >>= connect(...) >>= send(...);

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

Ответ 2

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

И вчера вечером я пришел и перечитал эти ответы. Самое главное, я перечитал конкретный пример С# в текстовых комментариях к видео Брайана Бекмана, о котором кто-то упоминал выше. Это было настолько ясно и понятно, что я решил опубликовать это прямо здесь.

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

Итак, вот комментарий - это прямая цитата из комментария, сделанного sylvan :

Это довольно круто. Это немного абстрактно, хотя. Я могу представить себе людей, которые не знают, какие монады уже запутались из-за отсутствия реальных примеров.

Итак, позвольте мне попытаться подчиниться, и просто чтобы быть действительно ясным, я сделаю пример на С#, даже если это будет выглядеть уродливо. Я добавлю эквивалентный Haskell в конце и покажу вам крутой синтаксический сахар Haskell, в котором, IMO, монады действительно начинают становиться полезными.

Итак, одна из самых простых монад в Хаскеле называется "Может быть, монада". В С# тип Maybe называется Nullable<T>. Это в основном крошечный класс, который просто инкапсулирует концепцию значения, которое либо является допустимым и имеет значение, либо является "нулевым" и не имеет значения.

Полезно придерживаться монады для объединения значений этого типа - понятие неудачи. Т.е. мы хотим иметь возможность просматривать несколько значений NULL и возвращать null как только любое из них будет NULL. Это может быть полезно, если вы, например, ищете много ключей в словаре или что-то еще, и в конце вы хотите обработать все результаты и как-то их объединить, но если какой-либо из ключей отсутствует в словаре, Вы хотите вернуть null для всего этого. Было бы утомительно вручную проверять каждый поиск на наличие null и возврата, поэтому мы можем скрыть эту проверку внутри оператора связывания (который является своего рода точкой монад, мы скрываем бухгалтерский учет в операторе связывания, что облегчает код использовать, так как мы можем забыть о деталях).

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

 class Program
    {
        static Nullable<int> f(){ return 4; }        
        static Nullable<int> g(){ return 7; }
        static Nullable<int> h(){ return 9; }


        static void Main(string[] args)
        {
            Nullable<int> z = 
                        f().Bind( fval => 
                            g().Bind( gval => 
                                h().Bind( hval =>
                                    new Nullable<int>( fval + gval + hval ))));

            Console.WriteLine(
                    "z = {0}", z.HasValue ? z.Value.ToString() : "null" );
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }
    }

Теперь, на мгновение, проигнорируйте, что уже есть поддержка для Nullable в С# (вы можете добавить nullable int вместе, и вы получите null, если любой из них равен null). Давайте представим, что такой функции нет, и это просто пользовательский класс без особой магии. Дело в том, что мы можем использовать функцию Bind чтобы связать переменную с содержимым нашего значения Nullable а затем сделать вид, что ничего странного не происходит, и использовать их как обычные целочисленные значения и просто сложить их вместе. Мы оборачиваем результат в nullable в конце, и этот nullable будет либо нулевым (если любой из f, g или h вернет null), либо будет результатом суммирования f, g и h вместе. (это аналогично тому, как мы можем связать строку в базе данных с переменной в LINQ и делать с ней что-то, Bind уверенными в том, что оператор Bind будет гарантировать, что переменная будет только когда-либо передавать допустимые значения строки).

Вы можете поиграть с этим и изменить любое из f, g и h чтобы вернуть ноль, и вы увидите, что все это вернет ноль.

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

Вот оператор Bind:

public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f ) 
    where B : struct 
    where A : struct
{
    return a.HasValue ? f(a.Value) : null;
}

Типы здесь такие же, как в видео. Он принимает M a (синтаксис Nullable<A> в N # в этом случае) и функцию от a до M b (Func<A, Nullable<B>> в синтаксисе С#) и возвращает M b (Nullable<B>).

Код просто проверяет, содержит ли nullable значение, и если это так, извлекает его и передает его в функцию, иначе он просто возвращает ноль. Это означает, что оператор Bind будет обрабатывать всю логику проверки нуля за нас. Если и только если значение, для которого мы вызываем Bind равно NULL, то это значение будет "передано" лямбда-функции, иначе мы выручим рано и все выражение будет нулевым. Это позволяет код, который мы пишем, используя монаду, чтобы быть полностью свободными от такого поведения нуля-проверки, мы используем только Bind и получить переменный, связанные со значением внутри монадическим значения (fval, gval и hval в примере коде), и мы может безопасно использовать их, зная, что Bind позаботится о проверке их на ноль, прежде чем передать их.

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

Наконец, здесь реализация того же кода в Haskell (-- начинается строка комментария).

-- Here the data type, it either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a

-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x

-- the "unit", called "return"
return = Just

-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
     g >>= ( \gval ->  
     h >>= ( \hval -> return (fval+gval+hval ) ) ) )

-- The following is exactly the same as the three lines above
z2 = do 
   fval <- f
   gval <- g
   hval <- h
   return (fval+gval+hval)

Как вы можете видеть, хорошая нотация do в конце делает ее похожей на простой императивный код. И действительно, это по замыслу. Монады можно использовать для инкапсуляции всех полезных вещей в императивном программировании (изменяемое состояние, ввод-вывод и т.д.) И использовать их с помощью этого приятного императивного синтаксиса, но за кулисами все это только монады и умная реализация оператора связывания! Круто то, что вы можете реализовать свои собственные монады, используя >>= и return. И если вы сделаете это, эти монады также смогут использовать нотацию do, что означает, что вы можете в основном писать свои собственные маленькие языки, просто определяя две функции!

Ответ 3

Монада - это, по существу, отсроченная обработка. Если вы пытаетесь написать код, который имеет побочные эффекты (например, ввод-вывод) на языке, который их не разрешает, и он разрешает только чистое вычисление, можно сказать, что "увернуться, я знаю, что вы не будете делать побочные эффекты для меня, но можете ли вы рассчитать, что произойдет, если вы это сделаете?"

Это своего рода обман.

Теперь, это объяснение поможет вам понять намерение больших картин монад, но дьявол находится в деталях. Как именно вы вычисляете последствия? Иногда это некрасиво.

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

Ответ 4

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

Ответ 5

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

Ответ 6

Посмотрите мой ответ на "Что такое монада?"

Он начинается с мотивирующего примера, работает через пример, выводит пример монады и формально определяет "монаду".

Он не знает знаний о функциональном программировании и использует псевдокод с синтаксисом function(argument) := expression с простейшими возможными выражениями.

Эта программа С# представляет собой реализацию псевдокодной монады. (Для справки: M - это конструктор типа, feed - операция привязки, а wrap - операция "return".)

using System.IO;
using System;

class Program
{
    public class M<A>
    {
        public A val;
        public string messages;
    }

    public static M<B> feed<A, B>(Func<A, M<B>> f, M<A> x)
    {
        M<B> m = f(x.val);
        m.messages = x.messages + m.messages;
        return m;
    }

    public static M<A> wrap<A>(A x)
    {
        M<A> m = new M<A>();
        m.val = x;
        m.messages = "";
        return m;
    }

    public class T {};
    public class U {};
    public class V {};

    public static M<U> g(V x)
    {
        M<U> m = new M<U>();
        m.messages = "called g.\n";
        return m;
    }

    public static M<T> f(U x)
    {
        M<T> m = new M<T>();
        m.messages = "called f.\n";
        return m;
    }

    static void Main()
    {
        V x = new V();
        M<T> m = feed<U, T>(f, feed(g, wrap<V>(x)));
        Console.Write(m.messages);
    }
}