В C # Что такое монада?

190

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

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

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

Спасибо!

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

Чарли Флауэрс
источник
Обратите внимание, что это на самом деле разработчик C # 3.0. Не путайте это с .NET 3.5. Помимо этого, хороший вопрос.
Раззи
4
Стоит отметить, что выражения запросов LINQ являются примером монадического поведения в C # 3.
Эрик Форбс
1
Я все еще думаю, что это дублирующий вопрос. Один из ответов в stackoverflow.com/questions/2366/can-anyone-explain-monads ссылается на channel9vip.orcsweb.com/shows/Going+Deep/… , где в одном из комментариев есть очень хороший пример C #. :)
jalf
4
Тем не менее, это всего лишь одна ссылка из одного ответа на один из вопросов SO. Я вижу ценность в вопросе, ориентированном на разработчиков на C #. Это то, что я хотел бы спросить у функционального программиста, который раньше занимался C #, если бы знал его, поэтому кажется разумным спросить об этом на SO. Но я уважаю и ваше право на ваше мнение.
Чарли Флауэрс
1
Разве один ответ не все, что вам нужно? ;) Суть в том, что у одного из других вопросов (а теперь и у этого тоже есть ответ) был
специфичный

Ответы:

147

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

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

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

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

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

STH
источник
28
Отличный ответ! Я собираюсь добавить цитату Оливера Стила, пытающегося связать монады с перегрузкой операторов, например, C ++ или C #: монады позволяют перегрузить ';' оператор.
Йорг Миттаг
6
@ JörgWMittag Я читал эту цитату раньше, но это звучало как чепуха. Теперь, когда я понимаю монады и читаю это объяснение того, как ';' это один, я понимаю. Но я думаю, что это действительно иррациональное утверждение для большинства разработчиков. ';' не рассматривается как оператор больше, чем // для большинства.
Джимми Хоффа
2
Вы уверены, что знаете, что такое монада? Монады это не "функция" или вычисление, есть правила для монад.
Луис
В вашем ;примере: какие объекты / типы данных ;отображаются? (Подумайте Listкарты Tв List<T>) Как ;карта морфизмов / функций между объектами / типами данных? Что pure, join, bindдля ;?
Миха Виденманн
44

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

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

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

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

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

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

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

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

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

Код просто проверяет, содержит ли nullable значение, и если это так, извлекает его и передает его в функцию, иначе он просто возвращает ноль. Это означает, что Bindоператор будет обрабатывать всю логику проверки нуля за нас. Если и только если значение, к которому мы обращаемся, не Bindравно нулю, то это значение будет «передано» лямбда-функции, иначе мы выручим рано и все выражение будет нулевым. Это позволяет коду , что мы пишем , используя монаду , чтобы быть полностью свободными от этого нулевой проверки поведения, мы просто используем 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запись в конце делает ее похожей на простой императивный код. И действительно, это по замыслу. Монады можно использовать для инкапсуляции всех полезных вещей в императивном программировании (изменяемое состояние, ввод-вывод и т. Д.) И использовать их с помощью этого приятного императивного синтаксиса, но за кулисами это всего лишь монады и умная реализация оператора связывания! Круто то, что вы можете реализовать свои собственные монады, используя >>=и return. И если вы сделаете это, эти монады также смогут использовать doнотацию, что означает, что вы можете в основном писать свои собственные маленькие языки, просто определяя две функции!

Чарли Флауэрс
источник
3
Лично я предпочитаю версию монады F #, но в любом случае они потрясающие.
ChaosPandion
3
Спасибо, что пришли сюда и обновили свой пост. Именно такие последующие действия помогают программистам, изучающим конкретную область, по-настоящему понять, как коллеги-программисты в конечном итоге относятся к этой области, вместо того, чтобы просто сказать «как я делаю х-у-технологию». Ты да человек!
Каппасим
Я пошел по тому же пути, что и вы, и пришел к тому же самому месту, понимая монады, и сказал, что это единственное лучшее объяснение связующего поведения монады, которое я когда-либо видел для императивного разработчика. Хотя я думаю, что вы не совсем касаетесь всего, что касается монад, что немного более подробно описано выше.
Джимми Хоффа
@ Джимми Хоффа - без сомнения, ты прав. Я думаю, чтобы по-настоящему понять их глубже, лучший способ - начать их много использовать и получать опыт . У меня еще не было такой возможности, но я надеюсь, что скоро.
Чарли Флауэрс
Кажется, что монада - это просто более высокий уровень абстракции с точки зрения программирования, или это просто непрерывное и недифференцируемое определение функции в математике. В любом случае, они не новая концепция, особенно в математике.
Лян
11

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

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

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

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

MarkusQ
источник
1
Как в книге «Я робот»? Где ученые просят компьютер рассчитать космические путешествия и попросить их пропустить определенные правила? :) :) :) :)
OscarRyz
3
Хм, Монаду можно использовать для отложенной обработки и для инкапсуляции побочных функций, действительно, это было первое настоящее приложение в Haskell, но на самом деле это гораздо более общий шаблон. Другие общие применения включают обработку ошибок и управление состоянием. Синтаксический сахар (например, в Haskell, Вычислительные выражения в F #, синтаксис Linq в C #) является просто таким и фундаментальным для монад как таковых.
Майк Хэдлоу
@MikeHadlow: экземпляры монады для обработки ошибок ( MaybeиEither e ) и управления состоянием ( State s, ST s) кажутся мне конкретными примерами «Пожалуйста, подсчитайте, что произойдет, если вы сделали [побочные эффекты для меня]». Другим примером будет недетерминизм ( []).
Пион
это совершенно верно; с одним (ну, двумя) дополнениями, что это E DSL, то есть встроенный DSL, поскольку каждое «монадическое» значение является допустимым значением самого «чистого» языка, что означает потенциально нечистое «вычисление». Кроме того, в вашем чистом языке существует монадическая конструкция «связывание», которая позволяет вам связывать чистые конструкторы таких значений, где каждый будет вызываться с результатом предыдущего вычисления, когда все комбинированное вычисление «выполняется». Это означает, что у нас есть возможность переходить на будущие результаты (или, в любом случае, на отдельную временную шкалу «выполнения»).
Уилл Несс
но для программиста это означает, что мы можем программировать в EDSL, смешивая его с чистыми вычислениями нашего чистого языка. стопка многослойных бутербродов представляет собой многослойный бутерброд. это что просто.
Уилл Несс
4

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

TheMissingLINQ
источник
1
Еще более полезным оказался комментарий, содержащий пример C # под видео.
Джалф
Я не знаю о более полезных, но это, безусловно, претворить идеи в жизнь.
TheMissingLINQ
0

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

хао
источник
Можете ли вы уточнить? Что это за интерфейс, который связывает его с монадой?
Джоэл Коухорн
2
Я думаю, что пост блога расширяет несколько параграфов, посвященных этому вопросу.
Хао
0

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

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

Он не предполагает никаких знаний о функциональном программировании и использует псевдокод с 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);
    }
}
Иордания
источник