Комментарии 31
1. Болшое количество условной логики
2. Исключения
Оба способа имеют очевидные недостатки
Чуть подробнее если можно
Скорее всего: исключения не бесплатны (слишком тяжелые если их подразумевается выбрасывать с больших обьемах), а на счет логики- пробрасывать типы ошибок в цепочке вызовов слишком муторная работа и отвлекает от понимания кода.
А если подойти с другой стороны, то для чего автор тащит монады в c#? Какую проблему они решают? "Основное назначение — инкапсуляция функций с побочным эффектом от чистых функций, а точнее, их выполнений от вычислений." (Определение из википедии). Тем самым мы отделяем логику обработки ошибок от самих типов.
IsNothing() — можно обработать только в самом конце цепочки вызовов, иначе пришлось бы делать проверку на каждом этапе, например:
int? Sum(string arg1, string arg2, string arg3)
{
int result = 0;
int tmp;
if(!int.TryParse(arg1, out tmp))
{
return null;
}
result += tmp;
if(!int.TryParse(arg2, out tmp))
{
return null;
}
result += tmp;
if(!int.TryParse(arg3, out tmp))
{
return null;
}
result += tmp;
return result;
}
вместо:
Maybe<int> Sum(string arg1, string arg2, string arg3)
{
int result = 0;
result += await MaybeParse(arg1);
result += await MaybeParse(arg2);
result += await MaybeParse(arg3);
}
2. А как понять источник «проблемы», если обнаружение этой проблемы происходит в самом конце?
3.
result += await MaybeParse(arg1);
Операцию сложения int с Maybe я ведь должен определять каждый раз? Или все операции с IsNothing() дают IsNothing()?
- Где-то обработать из IsNothing(), безусловно, придётся. Но это будет всего одна проверка вместо N+1
- Паттерн "Монада Maybe" применяется там где отсутствие (правильного?) значения не является проблемой и допускается логикой программы. Но, кстати, ничто не мешает добавить описание причины остановки выполнения цепочки вызова в MaybeResult, например:
Maybe<int> Parse(string str) => int.TryParse(str, out var result) ? result : Maybe<int>.Nothing($"Could not parse: {str}");
await MaybeParse(arg1)
возвращает int
2.
там где отсутствие (правильного?) значения не является проблемой и допускается логикой программы
Т.е. там где стоит if() на такое значение?
ничто не мешает добавить описание причины остановки выполнения
Ну т.е. надо внедрять протоколирование активно, как обычно, вы только об этом не написали ничего. А потом пойди разбери откуда какой str пришёл.
Вы это всё на практике применяете ежедневно?
Напишите дополнение про боли которые это всё доставляет в продакшене. Не внедряется это всё безболезненно.
3. А когда же MaybeParse() вернёт Maybe? Мы же про уход от if(), а это влечёт работу с Maybe
- Про исключения тут уже есть комментарии. Вкратце, не нужно их использовать для реализации программной логики.
- Maybe помогает избавится от избыточной условной логики, там где она действительно избыточна. Безусловно, можно привести 100500 примеров (с обоснованием), где применять Maybe не стоит, поэтому не надо применять Maybe (как, в принципе, и любой другой паттерн, подход, фреймворк и т. д.) там, где ее применять не стоит.
- MaybeParse() вернет Maybe, await MaybeParse() вернет либо int, либо завершит выполнение текущей асинхронной функции (в примере это "Sum") с результатом "Nothing" (если её тип Maybe). Если её тип не Maybe, а например Task то надо вызвать await MaybeParse().GetMaybeResult() что бы получить структуру в которой будет финальный результат или "Nothing".
Так эта статья о производительности?
2. Так в каких ситуациях это пользу принесёт? Или это теоретическая статья, вы про практику использования не написали.
Пожалейте новичков, пишите что это теория.
3. У вас был пример со сложением int + результат MaybeParse, почему-то вы не поняли в чём суть вопроса, забудем.
Постоянно пытаюсь из статей о ФП извлечь:
1. Чувство эстетического удовлетворения от применения ФП
2. И пользу для проектов и команды
Но каждая статья начинается с объяснения, что такое монады, зачем монады, примеры вида 1+2+3 и у меня ничего полезного не складывается в голове.
Хочется статью которая начинается с «Мы год использовали ФП, на таком то проекте… 2 члена команды чеканулись, 3 просветлели, ПМ стал улыбчивее, и мы сдали проект на 2 месяца раньше дедлайна».
Очень интересен именно такой опыт.
1) Ошибкой программе – тут поможет только выпуск новой версии с исправлением
2) Ошибкой конфигурации – например, неправильный Connection String
3) Внешней проблемой среды — например, отсутствует соединение с сетью или
нехватка памяти.
Все эти сценарии непредсказуемы и могут возникнуть при вызове абсолютно любой функции. Реакцией на них может быть только корректное завершение текущего логического действия (http запрос, событие onclick и т.д.) с извещением пользователя о нештатной ситуации.
В случае же когда ошибки допускаются логикой программы, например ошибка пользовательского ввода, то исключения использоваться не должны. Хотя, к сожалению, в реальности это происходит сплошь и рядом.
Что будет с using-ами внутри таких методов?
Условно
async MayBe<bool> WriteTo(string path)
{
using(var f = File.OpenWrite(path);
{
f.Write(Header, 0, Header.Length);
var data = await GetData();
f.Write(data, 0, data);
}
}
где GetData возвращает MayBe<byte[]>. Правильно ли я понимаю, что предложенное решение оставит метод в "подвешенном состоянии" и Dispose на файле не будет вызван?
Ну то есть предложенная методика вообще-то нарушает ряд гарантий языка и представляет из себя минное поле.
Не единственная. Там производительность идет далеко-далеко в лес.
Нет, не об этом.
При использовании исключений в синхронном коде будут затраты на выброс, раскрутку стека и отлов.
При использовании исключений в тасках можно ничего не бросать, а просто вернуть и потом проверять.
Но использование исключений в async-методах дает выброс-отлов на каждом (!) вызове в стеке.
Это гарантированный гроб производительности без вариантов.
Монады разве не разворачиваются как результат текущей монады + передача результата в следующую? Предположу что dispose будет работать как и следует, пока не получим исключение в цепочке вызовов. Скорее всего после выполнения последней цепочки и будет вызван dispose.
Ваш пример мог бы выглядеть вот так:
void Main()
{
foreach (var s in new[] {"1,2", "3,7,1", null, "1"})
{
// Map - это fmap из [1]
// Должно быть несколько перегрузок
// Maybe<TR> Map<TR>(Func<T, TR> transform)
// Maybe<TR> Map<TR>(Func<T, Maybe<TR>> transform)
// Вторая перегрузка нужна чтобы не получался дабл-Maybe
//
// Or - это аналог C# оператора ?? для Maybe<T>
// Опять же надо иметь несколько перегрузок
// T Or(T valueIfNothing) - тут все очевидно
// T Or(Func<T> valueFactoryIfNothing) - вызывать фабрику только ес. надо
// Maybe<T> Or(Maybe<T> valueIfNothing) - chaining - mb1.Or(mb2).Or(val)
// Maybe<T> Or(Func<Maybe<T>> valueFactoryIfNothing) - то же для фабрик
//
var res = Sum(s).Map(t => t.ToString()).Or("Nothing");
Console.WriteLine(res); // 3, 11, Nothing, Nothing
}
}
Maybe<int> Sum(string input)
{
// Здесь Map - это все еще fmap из [1], но теперь это extension method
// Принимает IEnumerable<Maybe<T>>, делает Map (см. выше) по-элементно
return Split(input).Map(Parse);
}
Maybe<string[]> Split(string str)
{
var parts = str?.Split(',').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
// Just.Nothing() - возвращает "пустую" (без полей) структуру Nothing
// Он должна быть implicitly convertible в "пустой" (без значения) Maybe<T>
// Не-дженерик Nothing тип это ОК: пустота и в Африке пустота ("не имеет" типа)
//
// ** Не очень понятно почему Split("1") возвращает Nothing, но у Вас именно так.
// Скопировал эту логику.
return parts == null || parts.Length < 2 ? Just.Nothing() : parts;
}
Maybe<int> Parse(string str)
{
return int.TryParse(str, out var result) ? result : Just.Nothing();
}
Поправочка для Sum — забыл сложение
Split(input).Map(int.Parse).Aggregate(Maybe.Apply((int x, int y) => x + y));
Maybe.Apply здесь возвращает функцию складывающую Maybe. Если любой из параметров пустой, оператор сложения не вызывается- вместо этого возвращается Nothing. Вместо сложения может быть что угодно.
C этим утверждением согласен, поскольку именно Maybe на 100% правильно реализовать через await не удается, об этом сказано в конце статьи. Но, все же вариант с await, мне кажется, лучше, поскольку избавляет нас от лямбд, что положительно сказывается на читабельности программы.
Ну лямбды можно выносить в функции если не гнаться за однострочечностью
Split(input).Map(int.Parse).Aggregate(Maybe.Apply(Sum);
А особо популярные паттерны выносить в экстеншен методы
Split(input).Map(int.Parse).MaybeAggregate(Sum);
Или даже
Split(input).Map(int.Parse).MaybeSum()
Это в целом от стиля зависит: Maybe + Map + Apply + Bind + обвязка над стандартным LINQ типа MaybeAggregate позволяют творить LINQ-чудеса и склеивать функции принимающие Maybe и обычные аргументы произвольным образом. Я просто не люблю foreach и циклы для меня функциональный LINQ-style код читается сильно проще. Обратите внимание что последний пример читается почти как полноценное как английское преложение
ФП это хорошо, но с учетом экосистемы и системы типов сишарпа писать на нем будет больно. Плюсы парадигмы не перевесят каждодневного ада. Все равно что ООП в pure C. Берите подходящий язык, и все будет хорошо. А иначе только боль и страдания.
Монада «Maybe» через async/await в C# (без Task-oв!)