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