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