Слово «монада» звучит так, будто пришло из учебника по высшей математике. Если вы писали на C#, вы уже пользовались монадами.
Task<T>, Nullable<T>, IEnumerable<T> и весь LINQ построены на этом паттерне.Монада в двух словах
Монада это обёртка над значением, которая умеет делать две вещи.
Первая. Оборачивать значение в контейнер. В C# это конструктор или фабричный метод. Например,
Task.FromResult(42) оборачивает число 42 в Task<int>.Вторая. Прокидывать значение через цепочку операций, где каждая операция тоже возвращает обёртку. В функциональном программировании эту операцию называют
Bind. В C# она реализована как SelectMany.Формально монада должна соблюдать три закона: левая единица, правая единица, ассоциативность; но для повседневной работы достаточно понимать саму идею цепочки.
Nullable как монада
Nullable<T> это контейнер, который либо содержит значение, либо пуст. До C# 8 приходилось писать вложенные проверки на null:int? a = GetValue();
int? b = null;
if (a.HasValue)
{
b = Transform(a.Value);
}
Монадный подход позволяет выстроить цепочку без ручных проверок. Напишем свой
Bind для Nullable<T>:public static T? Bind<T>(this T? source, Func<T, T?> func)
where T : struct
{
return source.HasValue ? func(source.Value) : null;
}
Теперь вместо вложенных if мы получаем цепочку:
int? result = GetValue()
.Bind(x => Multiply(x, 2))
.Bind(x => Add(x, 10));
Если на любом шаге значение окажется null, вся цепочка вернёт null. Никаких проверок вручную.
Task как монада
Task<T> работает по тому же принципу. Метод ContinueWith это грубый аналог Bind, но гораздо удобнее пользоваться async/await. Компилятор сам выстраивает цепочку продолжений:
var user = await GetUserAsync(id);
var orders = await GetOrdersAsync(user);
var total = await CalculateTotalAsync(orders);
Каждый
await разворачивает Task<T>, передаёт значение в следующую операцию и снова заворачивает результат в Task. Это и есть монадная цепочка, просто записанная через синтаксический сахар.LINQ и SelectMany
LINQ query syntax это буквально монадный синтаксис.
Ключевое слово
from вызывает SelectMany под капотом:var pairs =
from x in new[] { 1, 2, 3 }
from y in new[] { "a", "b" }
select (x, y);
Компилятор превращает это в вызов
SelectMany:var pairs = new[] { 1, 2, 3 }
.SelectMany(
x => new[] { "a", "b" },
(x, y) => (x, y));SelectMany берёт каждый элемент, применяет функцию, которая возвращает новую коллекцию, и склеивает всё в один плоский результат. Это и есть Bind для IEnumerable<T>.Свой тип Result как монада
В реальных проектах монады полезны для обработки ошибок без исключений.
Напишем простой
Result<T>:public class Result<T>
{
public T Value { get; }
public string Error { get; }
public bool IsSuccess { get; }
private Result(T value)
{
Value = value;
IsSuccess = true;
}
private Result(string error)
{
Error = error;
IsSuccess = false;
}
public static Result<T> Ok(T value) => new(value);
public static Result<T> Fail(string error) => new(error);
public Result<TOut> Bind<TOut>(Func<T, Result<TOut>> func)
{
return IsSuccess ? func(Value) : Result<TOut>.Fail(Error);
}
}
Теперь цепочка бизнес-логики выглядит так:
var result = Validate(input)
.Bind(SaveToDatabase)
.Bind(SendNotification);
Если валидация упала,
SaveToDatabase и SendNotification не выполнятся. Ошибка пробросится по цепочке без try/catch и без вложенных if.Когда вы видите повторяющийся паттерн «проверь, разверни, передай, заверни обратно», скорее всего перед вами монада. И вместо ручного кода для каждого случая можно написать один
Bind и выстроить цепочку.📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#il_люминатор