Помогите разработчику C # понять: что такое монада? - PullRequest
183 голосов
/ 23 марта 2009

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

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

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

Спасибо!

(РЕДАКТИРОВАТЬ: Кстати, я знаю, что по крайней мере 3 вопроса "что такое монада" уже есть на SO. Однако я сталкиваюсь с той же проблемой с ними ... так что этот вопрос необходим, потому что из-за C # -разработчик. Спасибо.)

Ответы [ 6 ]

146 голосов
/ 24 марта 2009

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

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

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

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(...);

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

42 голосов
/ 20 марта 2010

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

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

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

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

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

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

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

Полезная вещь в монаде для объединения значений этого тип это понятие провала. То есть мы хотим иметь возможность смотреть на несколько обнуляемых значений и возвращают 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 в C # (вы можете добавить обнуляемые целые числа вместе, и вы получите нуль, если либо ноль). Давайте представим, что такой функции нет, и это просто пользовательский класс без особой магии. Дело в том что мы можем использовать функцию Bind, чтобы связать переменную с содержимым нашего Nullable значения, а затем делать вид, что нет ничего странного продолжать, и использовать их как обычные целые и просто сложить их вместе. Мы оберните результат в конец обнуляемым, и этот обнуляемый будет либо будет нулевым (если любой из f, g или h вернет нулевой), либо будет результат суммирования f, g и h вместе. (это аналог о том, как мы можем связать строку в базе данных с переменной в LINQ, и сделать наберитесь этого, уверенного в том, что оператор Bind убедитесь, что в переменную будет передана только допустимая строка значения).

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

Так что оператор связывания должен сделать эту проверку за нас, и поручиться возвращать нуль, если он встречает нулевое значение, и в противном случае передать вдользначение внутри структуры 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> в синтаксисе C # для этого случая) и функция от a до M b (Func<A, Nullable<B>> в синтаксисе C #) и возвращает M b (Nullable<B>).

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

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

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

-- Here's the data type, it's 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 выглядит так прямой императивный код. И действительно, это по замыслу. Монады могут быть используется для инкапсуляции всего полезного в императивном программировании (изменяемое состояние, IO и т. д.) и используется с помощью этого хорошего императивного Синтаксис, но за кулисами, это все просто монады и умный реализация оператора связывания! Самое классное, что вы можете реализовать свои собственные монады путем реализации >>= и return. И если вы делаете так, что эти монады также смогут использовать запись do, Это означает, что вы можете написать свои собственные маленькие языки, просто определяя две функции!

11 голосов
/ 23 марта 2009

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

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

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

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

4 голосов
/ 23 марта 2009

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

0 голосов
/ 09 марта 2015

См. Мой ответ на "Что такое монада?"

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

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

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

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);
    }
}
0 голосов
/ 23 марта 2009

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

...