Прошел год с тех пор, как я разместил этот вопрос. После публикации я погрузился в Haskell на пару месяцев. Мне это очень понравилось, но я отложил его в сторону, как только я был готов погрузиться в Монады. Я вернулся к работе и сосредоточился на технологиях, необходимых для моего проекта.
Это довольно круто. Это немного абстрактно, хотя. Я могу представить людей
кто не знает, какие монады уже запутались из-за отсутствия
реальные примеры.
Итак, позвольте мне попытаться подчиниться, и просто чтобы быть действительно ясным, я сделаю
пример в C #, хотя это будет выглядеть ужасно. Я добавлю эквивалент
Haskell в конце и покажу вам крутой синтаксический сахар Haskell, который
ИМО, монады действительно начинают приносить пользу.
Хорошо, поэтому одна из самых простых монад называется "Может быть, монада" в
Haskell. В C # тип Maybe называется Nullable<T>
. Это в основном
крошечный класс, который просто инкапсулирует концепцию значения, которое
либо допустимо и имеет значение, либо равно нулю и не имеет значения.
Полезная вещь в монаде для объединения значений этого
тип это понятие провала. То есть мы хотим иметь возможность смотреть на
несколько обнуляемых значений и возвращают 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
значения, а затем делать вид, что нет ничего странного
продолжать, и использовать их как обычные целые и просто сложить их вместе. Мы
оберните результат в конец обнуляемым, и этот обнуляемый будет
либо будет нулевым (если любой из f
, g
или h
вернет нулевой), либо будет
результат суммирования f
, g
и h
вместе. (это аналог
о том, как мы можем связать строку в базе данных с переменной в LINQ, и сделать
наберитесь этого, уверенного в том, что оператор Bind
убедитесь, что в переменную будет передана только допустимая строка
значения).
Вы можете поиграть с этим и изменить любое из f
, g
и h
, чтобы вернуться
NULL, и вы увидите, что все вернется 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
on не равен NULL, тогда это значение будет «передано»
лямбда-функция, иначе мы выручим рано, и все выражение
ноль. Это позволяет коду, который мы пишем с использованием монады,
полностью свободен от этого поведения проверки нуля, мы просто используем 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
,
Это означает, что вы можете написать свои собственные маленькие языки, просто
определяя две функции!