Прошел год с тех пор, как я разместил этот вопрос. После публикации я погрузился в Haskell на пару месяцев. Мне это очень понравилось, но я отложил его в сторону, как только я был готов погрузиться в Монады. Я вернулся к работе и сосредоточился на технологиях, необходимых для моего проекта.
Это довольно круто. Это немного абстрактно, хотя. Я могу представить себе людей, которые не знают, какие монады уже запутались из-за отсутствия реальных примеров.
Итак, позвольте мне попытаться подчиниться, и просто чтобы быть действительно ясным, я сделаю пример на 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нотацию, что означает, что вы можете в основном писать свои собственные маленькие языки, просто определяя две функции!