Pull to refresh

Comments 31

1. Болшое количество условной логики
2. Исключения
Оба способа имеют очевидные недостатки

Чуть подробнее если можно

Скорее всего: исключения не бесплатны (слишком тяжелые если их подразумевается выбрасывать с больших обьемах), а на счет логики- пробрасывать типы ошибок в цепочке вызовов слишком муторная работа и отвлекает от понимания кода.

Я могу придумать кучу недостатков, но интересно что автор имеет в виду :)

А если подойти с другой стороны, то для чего автор тащит монады в c#? Какую проблему они решают? "Основное назначение — инкапсуляция функций с побочным эффектом от чистых функций, а точнее, их выполнений от вычислений." (Определение из википедии). Тем самым мы отделяем логику обработки ошибок от самих типов.

Кроме того, можно и вовсе забыть сделать проверку возвращаемого значения, и получить какой-нибудь NullReferenceException в Runtime-е (это в лучшем случае)
А в случае если результат будет IsNothing(), то в рантайме мы что получим без его обработки?

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);            
}
1. Так ошибка в рантайме всёравно будет (или на этапе передачи в СУБД или ещё куда-то) или надо где-то всёравно обработать IsNothing().
2. А как понять источник «проблемы», если обнаружение этой проблемы происходит в самом конце?
3.
result += await MaybeParse(arg1);

Операцию сложения int с Maybe я ведь должен определять каждый раз? Или все операции с IsNothing() дают IsNothing()?
  1. Где-то обработать из IsNothing(), безусловно, придётся. Но это будет всего одна проверка вместо N+1
  2. Паттерн "Монада Maybe" применяется там где отсутствие (правильного?) значения не является проблемой и допускается логикой программы. Но, кстати, ничто не мешает добавить описание причины остановки выполнения цепочки вызова в MaybeResult, например:
    Maybe<int> Parse(string str)
    => int.TryParse(str, out var result) 
        ? result 
        : Maybe<int>.Nothing($"Could not parse: {str}");
  3. await MaybeParse(arg1) возвращает int
1. Так и исключение только один раз можно обработать
2.
там где отсутствие (правильного?) значения не является проблемой и допускается логикой программы

Т.е. там где стоит if() на такое значение?
ничто не мешает добавить описание причины остановки выполнения

Ну т.е. надо внедрять протоколирование активно, как обычно, вы только об этом не написали ничего. А потом пойди разбери откуда какой str пришёл.
Вы это всё на практике применяете ежедневно?
Напишите дополнение про боли которые это всё доставляет в продакшене. Не внедряется это всё безболезненно.
3. А когда же MaybeParse() вернёт Maybe? Мы же про уход от if(), а это влечёт работу с Maybe
  1. Про исключения тут уже есть комментарии. Вкратце, не нужно их использовать для реализации программной логики.
  2. Maybe помогает избавится от избыточной условной логики, там где она действительно избыточна. Безусловно, можно привести 100500 примеров (с обоснованием), где применять Maybe не стоит, поэтому не надо применять Maybe (как, в принципе, и любой другой паттерн, подход, фреймворк и т. д.) там, где ее применять не стоит.
  3. MaybeParse() вернет Maybe, await MaybeParse() вернет либо int, либо завершит выполнение текущей асинхронной функции (в примере это "Sum") с результатом "Nothing" (если её тип Maybe). Если её тип не Maybe, а например Task то надо вызвать await MaybeParse().GetMaybeResult() что бы получить структуру в которой будет финальный результат или "Nothing".
1. Вы про производительность? Ну а не надо их бросать в цикле от которого ожидается высокая производительность.
Так эта статья о производительности?
2. Так в каких ситуациях это пользу принесёт? Или это теоретическая статья, вы про практику использования не написали.
Пожалейте новичков, пишите что это теория.
3. У вас был пример со сложением int + результат MaybeParse, почему-то вы не поняли в чём суть вопроса, забудем.

Постоянно пытаюсь из статей о ФП извлечь:
1. Чувство эстетического удовлетворения от применения ФП
2. И пользу для проектов и команды
Но каждая статья начинается с объяснения, что такое монады, зачем монады, примеры вида 1+2+3 и у меня ничего полезного не складывается в голове.
Хочется статью которая начинается с «Мы год использовали ФП, на таком то проекте… 2 члена команды чеканулись, 3 просветлели, ПМ стал улыбчивее, и мы сдали проект на 2 месяца раньше дедлайна».
Очень интересен именно такой опыт.
UFO just landed and posted this here
Когда вариантов исключений / типов возможных возвращаемых ошибок (именно типов, а не значений некоторого типа) становится слишком много и их надо протащить через сигнатуру, это тоже становится проблемой.
А зачем их видеть в сигнатурах функций? Исключения, должны (в теории) сигнализировать о нештатной работе программы, которая может быть вызвана:

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 на файле не будет вызван?

Если GetData() НЕ вернет Mabe.Nothing, то Dispose будет вызван, в противном же случае, вызова Dispose не будет. Об этом есть пара предложений в конце статьи

Ну то есть предложенная методика вообще-то нарушает ряд гарантий языка и представляет из себя минное поле.

Не единственная. Там производительность идет далеко-далеко в лес.

Нет, не об этом.
При использовании исключений в синхронном коде будут затраты на выброс, раскрутку стека и отлов.
При использовании исключений в тасках можно ничего не бросать, а просто вернуть и потом проверять.
Но использование исключений в async-методах дает выброс-отлов на каждом (!) вызове в стеке.
Это гарантированный гроб производительности без вариантов.

Монады разве не разворачиваются как результат текущей монады + передача результата в следующую? Предположу что dispose будет работать как и следует, пока не получим исключение в цепочке вызовов. Скорее всего после выполнения последней цепочки и будет вызван dispose.

ИМХО, именно Maybe лучше реализовывать без await. Я когда-то делал это используя вот эту статью как источник вдохновения — habr.com/ru/post/183150 [1]. Просто брал и максимально близко переписывал Haskell «методы» на C#

Ваш пример мог бы выглядеть вот так:
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. Вместо сложения может быть что угодно.

«именно Maybe лучше реализовывать без await»

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 код читается сильно проще. Обратите внимание что последний пример читается почти как полноценное как английское преложение

Идея кстати та же — IEnumerable это монада, как и async/await

ФП это хорошо, но с учетом экосистемы и системы типов сишарпа писать на нем будет больно. Плюсы парадигмы не перевесят каждодневного ада. Все равно что ООП в pure C. Берите подходящий язык, и все будет хорошо. А иначе только боль и страдания.

Sign up to leave a comment.

Articles