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


190

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

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

Однак, напевно, можна передати концепцію? Принаймні, я на це сподіваюся. Може бути , ви можете уявити C # приклад в якості основи, а потім описати те , що # розробник C б побажати він міг би зробити звідти , але не може , тому що мова не вистачає функціональних можливостей програмування. Це було б фантастично, оскільки воно передавало б наміри та переваги монад. Тож ось моє запитання: яке найкраще пояснення ви можете дати монадам розробнику C # 3?

Дякую!

(EDIT: До речі, я знаю, що принаймні три питання "що таке монада" вже є на ЗО. Однак я зіткнувся з тією ж проблемою і з ними ... тому це питання потрібно imo через C # -девелопера фокус. Дякую.)


Зверніть увагу, що це насправді розробник C # 3.0. Не помиляйтеся з .NET 3.5. Крім того, приємне запитання.
Razzie

4
Варто зазначити, що вирази запитів LINQ є прикладом монадичної поведінки в C # 3.
Ерік Форбс

1
Я все ще думаю, що це повторне питання. Один з відповідей на stackoverflow.com/questions/2366/can-anyone-explain-monads посилання на channel9vip.orcsweb.com/shows/Going+Deep / ... , де один з коментарів , має дуже хороший C # приклад. :)
jalf

4
Все-таки, це лише одне посилання від однієї відповіді до одного з питань ТА. Я бачу цінність у питанні, орієнтованому на розробників C #. Я б попросив функціонального програміста, який раніше робив C #, якби я знав його, тому здається розумним запитати його на ТАК. Але я також поважаю ваше право на вашу думку.
Charlie Flowers

1
Чи не одна відповідь на все, що вам потрібно? ;) Моя точка зору полягає тільки , що один з інших питань (і тепер це один , як добре, так яй) мав C # Певні відповідь (який , здається , дуже добре написано, на самому ділі Ймовірно, краще пояснення , яке я бачив).
jalf

Відповіді:


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

Офіційне визначення дещо складніше, тому що ви повинні турбуватися про те, як отримати результат однієї функції як вхід до наступної, якщо ця функція потребує цього вводу і оскільки ви хочете переконатися, що функції, які ви поєднуєте, підходять до як ви намагаєтеся поєднати їх у монаді. Але основна концепція полягає лише в тому, що ви формалізуєте різні способи комбінування функцій разом.


28
Чудова відповідь! Я збираюся навести цитату Олівера Стіла, намагаючись зв’язати Monads з перевантаженням оператора à la C ++ або C #: Monads дозволяють перевантажувати ';' оператор.
Йорг W Міттаг

6
@ JörgWMittag Я читав цю цитату раніше, але це звучало як надмірно сувора дурниця. Тепер, коли я розумію монади і читаю це пояснення, як ';' це одне, я розумію. Але я думаю, що це дійсно нераціональне твердження для більшості імперативних розробників. ';' більшість не сприймається як оператор, ніж // є.
Джиммі Хоффа

2
Ви впевнені, що знаєте, що таке монада? Монади - це не «функція» чи обчислення, є правила для монад.
Луїс

У вашому ;прикладі: Які об’єкти / типи даних відображає ;карта? (Подумайте Listкарти Tдля List<T>) Як ;відображається морфізм / функції між об'єктами / типами даних? Що pure, join, bindдля ;?
Micha Wiedenmann

44

Минув рік, як я опублікував це питання. Опублікувавши це, я заглибився в Haskell на пару місяців. Мені надзвичайно сподобалось, але я відклав це так, як я був готовий заглибитися в Монади. Я повернувся до роботи і зосередився на технологіях, необхідних моєму проекту.

І минулої ночі я прийшов і перечитав ці відповіді. Найголовніше , що я перечитав конкретний приклад C # у текстових коментарях відео Брайана Бекмана, який хтось згадує вище . Це було настільки чітко і яскраво, що я вирішив опублікувати його тут.

Через цей коментар я не тільки відчуваю, що я точно розумію , що таке Монади… Я розумію, що насправді написав деякі речі в C #, які є Монадами… або, принаймні, дуже близькими, і прагну вирішити ті самі проблеми.

Отже, ось коментар - це все пряма цитата з коментаря тут від sylvan :

Це досить класно. Це трохи абстрактно. Я можу собі уявити людей, які не знають, які монади вже плутаються через відсутність реальних прикладів.

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

Гаразд, тому одного з найпростіших монадів у Хаскелі називають "Монадою". В C # називається тип Можливо 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 # вже є підтримка для цього (ви можете додати нульові вставки разом, і ви отримаєте null, якщо будь-який є null). Давайте зробимо вигляд, що такої функції немає, і це просто визначений користувачем клас без особливої ​​магії. Справа в тому, що ми можемо використовувати Bindфункцію, щоб прив’язати змінну до змісту нашого Nullableзначення, а потім зробимо вигляд, що нічого дивного не відбувається, і використовувати їх як звичайні вставки та просто додавати їх разом. Ми обернути результат в обнуляє врешті-решт, і що обнуляти буде або нульова (якщо якісь - або з f, gабо hповертає нуль) або це буде результатом підсумовування f, gіhразом. (це аналогічно тому, як ми можемо прив'язувати рядок у базі даних до змінної в LINQ і робити це з нею, безпечно, знаючи, що Bindоператор переконається, що змінна коли-небудь передасть дійсні значення рядка).

Ви можете грати з цим і змінити будь-який f, gі hповертати нуль , і ви побачите , що все це буде повертати нуль.

Отож, чітко оператор прив'язки повинен зробити це для нас, перевіряючи, і виправити повернення 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, не є нульовим, то це значення буде "передано" до функції лямбда, інакше ми накопичуємо рано і весь вираз є нульовим. Це дозволяє коду , що ми пишемо , використовуючи Монада , щоб бути повністю вільними від цього нульовий перевірки поведінки, ми просто використовуємо 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позначення, а це означає, що ви можете в основному писати свої власні мови, просто визначивши дві функції!


3
Особисто я віддаю перевагу F # 'монадію версії, але в будь-якому випадку вони є приголомшливими.
ChaosPandion

3
Дякуємо, що повернулися сюди та оновили свою публікацію. Це такі дії, які допомагають програмістам, які дивляться в певну область, дійсно розуміють, як колеги-програмісти в кінцевому рахунку розглядають цю область, замість того, щоб просто "як мені зробити х у технологіях", щоб піти. Ти, чоловіче!
kappasims

Я в основному пройшов той самий шлях, який ви пройшли, і прийшов до того самого місця, розуміючи монад, що сказав, що це єдине найкраще пояснення обов'язкової поведінки монади, яку я коли-небудь бачив для імперативного розробника. Хоча я думаю, ви не зовсім зачіпаєте все про монади, що трохи більше пояснено вище, ніж що-небудь.
Джиммі Хоффа

@Jimmy Hoffa - без сумніву, ти маєш рацію. Я думаю, щоб по-справжньому зрозуміти їх глибше, найкращий спосіб - почати їх багато використовувати і отримати досвід . Я ще не мав такої можливості, але сподіваюся, що скоро.
Charlie Flowers

Здається, монада - це просто вищий рівень абстракції з точки зору програмування, або це просто неперервне і недиференційоване визначення функцій у математиці. Так чи інакше, вони не нова концепція, особливо з математики.
laang

11

Монада є по суті відкладеною обробкою. Якщо ви намагаєтеся написати код, який має побічні ефекти (наприклад, введення / виведення) мовою, яка їх не дозволяє, і дозволяє лише чисті обчислення, одним ухиленням буде сказати: "Гаразд, я знаю, що ви не зробите побічних ефектів для мене, але чи можете ви, будь ласка, обчислити, що було б, якби ви це зробили?

Це свого роду обман.

Тепер це пояснення допоможе вам зрозуміти великі наміри монад, але чорт у деталях. Як точно чи ви обчислити наслідки? Іноді це не дуже.

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


1
Як у книзі I Robot? Де вчений запитує комп’ютер для обчислення космічної подорожі та просить їх пропустити певні правила? :) :) :) :)
OscarRyz

3
Хм, монаду можна використовувати для відкладеної обробки та інкапсуляції функцій, що впливають на бік, і справді це було першим реальним застосуванням у Haskell, але це насправді набагато більш загальна модель. Інші поширені сфери використання включають обробку помилок та управління державою. Синтаксичний цукор (робити в Haskell, обчислювальні вирази в F #, синтаксис Linq в C #) є лише таким і основним для Monads як такого.
Майк Хадлоу

@MikeHadlow: Монадні екземпляри для керування помилками ( Maybeта Either e) та управління державою ( State s, ST s) вважають мене окремими випадками "Прошу обчислити, що буде, якщо ти зробив би [побічні ефекти для мене]". Іншим прикладом може бути недетермінізм ( []).
піон

це точно так; з одним (ну, двома) доповненнями, що це E DSL, тобто вбудований DSL, оскільки кожне "монадичне" значення є дійсним значенням самої вашої "чистої" мови, що є потенційно нечистим "обчисленням". Крім того, на вашій чистій мові існує монадійна конструкція "прив'язування", яка дозволяє ланцюжком чистих конструкторів таких значень, коли кожен буде викликаний результатом попереднього обчислення, коли "всі комбіновані обчислення" "запущені". Це означає, що ми маємо можливість розгалужувати майбутні результати (або в будь-якому випадку окремої часової шкали "запустити").
Буде Несс

але для програміста це означає, що ми можемо програмувати в EDSL, одночасно змішуючи його з чистими розрахунками нашої чистої мови. стопка багатошарових бутербродів - це багатошаровий сендвіч. це що просто.
Чи буде Несс

4

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


1
Що мені було ще корисніше, це коментар, який містить приклад C # під відео.
jalf

Я НЕ знаю про більш корисним, але це , звичайно , покласти ідеї на практиці.
TheMissingLINQ

0

Ви можете думати про монаду як C #, interfaceяку повинні реалізувати класи . Це прагматична відповідь, яка ігнорує всю теоретичну математику категорії, чому ви хочете вибрати ці декларації у своєму інтерфейсі, і ігнорує всі причини, чому ви хочете мати монади мовою, яка намагається уникати побічних ефектів, але я знайшов це гарним початком як хтось, хто розуміє (C #) інтерфейси.


Чи можете ви докладно? Що це за інтерфейс, який відноситься до монад?
Joel Coehoorn

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);
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.