Pull to refresh

Comments 21

Не знаю почему многие так любят Linq синтаксис. Всегда ему предпочитал последовательный вызов методов.
Более высокоуровневые конструкии — ближе к человеческому языку, поэтому читаются легче, чем циклы с хитрыми счетчиками и итераторами.

Зачем пытаться из C# сделать F#?
Тем более этот SQL синтаксис


from x in xs select x

выглядит чужеродно для не коллекций.

Чего только люди ни придумают лишь бы класс Exception не использовать. А в нем уже есть все для обработки ошибок.
И про исключения очередные мифы дядюшки Римуса.
Что конкретно вы считаете «мифом»?
Что конкретно вы считаете «мифом»?

Все типовые доводы против исключений в вашей статье.
Возьмем один самых распространенных и вредных:


ошибки – объекты первого класса

Нет. Объекты первого класса — данные, ошибки — просчеты при работе с данными, дефекты — разница между неверными и правильными данными, сбой — результат обработки неверных данных без коррекции.
Это частный случай более общего мифа о равноценности отработки основного алгоритма и обработке сбоев.
На деле же все наоборот:


  1. Если не реализован основной алгоритм — у вас вообще ничего нет, никакого продукта и никакой ценности не создано. Про обработку ошибок речь даже не заходит.
  2. Если не реализована обработка ошибок — вы несете убытки при сбоях. Это может быть очень неприятно, но программа без обработки ошибок все равно имеет определенную ценность. Более того, в львиной доле вариантов использования этого вполне достаточно.
  3. Сложность основного алгоритма в общем случае — существенная. Сложность обработки ошибок даже в самых запутанных случаях — акцидентная по Бруксу.
ошибки – объекты первого класса

Вы вырвали из контекста. Полная фраза такая:

Является ли любая ошибка «исключительной ситуацией»? Если вы когда-нибудь сталкивались с бухгалтерским или налоговым учетом, то наверняка знаете, что существует специальный термин «корректировка». Он означает, что в прошлом отчетном периоде были поданы неверные сведения и их необходимо исправить. То есть в сфере учета, без которой бизнес не может существовать в принципе, ошибки – объекты первого класса. Для них введены специальные термины. Можно ли назвать их исключительными ситуациями? Нет. Это нормальное поведение. Люди ошибаются.

В налоговом и бух.учетах «корректировки» — такие же участники предметной области как и «отчетный период» и «подаваемые сведения». Более того, если налоговая находит ошибки в отчетности, то вы обязаны подать корректировку и оплатить штраф. Т.е. для обработки ошибок есть отдельный бизнес-процесс. Это не миф, это реальность в странах, где существует налоговая система, коих большинство.

Если не реализована обработка ошибок — вы несете убытки при сбоях. Это может быть очень неприятно, но программа без обработки ошибок все равно имеет определенную ценность. Более того, в львиной доле вариантов использования этого вполне достаточно

Knight capital потеряла $440.000.000 за 30 минут из-за ошибки. Да она была не в программе, а из-за человеческого фактора при выкладке. Кто знает, если бы в их ПО была система самодиагностики оно бы аварийно завершилось и компания понесла бы убытки, но не обанкротилась. Один из наших клиентов занимается разработкой медицинских тренажеров. Стоимость ошибок в их отрасли — человеческие жизни. «Львиная доля» — понятие весьма субъективное. Если в сфере, где вы работает нужно, чтобы программа «делала вид, что как-то работает», это не значит, что критерии успешности других людей совпадают с вашими.

Сложность основного алгоритма в общем случае — существенная. Сложность обработки ошибок даже в самых запутанных случаях — акцидентная по Бруксу.

Если бы эта сложнасть была «акцидентная», Брукс не предлагал бы тратить половину (Карл!) времени на системное тестирование и отладку. Да не все это время уйдет на исправлние именно ошибок. Очень много «съест» работа с изменениями требований. Ноги этой проблемы все-равно растут из нежелания разбираться с «побочными» путями выполнения программы: тут не учли, здесь забыли, тут доработочка. Сдвинем ка релиз на пол года.

ФП стиль обработки исключений хорош, но когда твой ЯП набит сайд-эффектами под завязку, он ничем не поможет, особенно когда весь остальной C# мир (который будет использовать твой код и чей код ты используешь) использует try/catch/finally trow
Railway стиль хорош, но для C# не родной. Имеет право на жизнь внутри конвейерной обработки на internal уровне в библиотеках, так как способен уменьшить количество объявленных Exception type. Однако, во первых — Result должен быть структурой, чтоб на нуль не проверять. Result должен быть Ether c Left type = Exception, должен испльзовать моноидную природу AggregateException, и нужна Try обертка над обычными функциями. Тогда возможна такая запись:
Try(() => File.Create(«abc»)).Correct(() => File.Create(«dfg»)).Bind(p => p.Write(a)).AndAlso(p => p.Close);
Почему так сразу и резко не родной?
Task содержит в себе все что нужно.
Конечно, размеченных объединений и нормальной do-нотации не хватает, но сделать можно немало.

Сначала вводим тип-объединение, чтобы заставить вызывающий код проверять ошибки. А потом используем слово на букву М, чтобы их тупо игнорировать. Что-то тут не так..

Вы их не игнорируете. Если не использовать Return вы не вынете значение из Value, значит придется обработать и успешное завершение и ошибку.

А вы не пробовали F#? Там уже из коробки есть Discriminated Union и монструозные конструкции превращаются в:


type MyResult<'T> = 
    | Success of 'T
    | Error   of exn list

//Это ваш Select
let map f = function
    | Success v -> Success (f v)
    | x -> x

//Это ваш SelectMany
let bind f = function
    | Success v -> f v
    | x -> x

//Это ваш SelectMany от 2 функций
let bind2 f g = bind (fun x -> f x |> bind g)

А вообще в F# уже есть готовый тип Result с map, bind и пр, поэтому это всё не пригодилось бы.
Если у вас такая сложная доменная логика, вы можете отдельный проект под неё на F# запилить и 90% кодовой базы уйдёт за ненадобностью.
Меньше кода — меньше ошибок. А у вас за дженерик параметрами "леса не видно":


public static Result<TDestination>
    SelectMany<TSource, TIntermediate, TDestination>(
    this Result<TSource> result,
    Func<TSource, Result<TIntermediate>> inermidiateSelector,
    Func<TSource, TIntermediate, TDestination> resultSelector)
    => result.SelectMany<TSource, TDestination>(s => inermidiateSelector(s)
        .SelectMany<TIntermediate, TDestination>(m => resultSelector(s, m)));
Вы все правильно поняли. Этот Result — калька с F# с поправкой на реалии C#. Union заменил на T,Failure. Метод Return, чтобы обязать проверить оба варианта, потому что компилятор C# не предупреждает, если не все случаи обработаны в pattern matching. Приходится выкручиваться. LINQ-синтаксис — замена computation expressions.

Не всегда можно добавить в стек ещё один ЯП, зачастую по организационным причинам. Зато можно какие-то инструменты портировать, хотя они и могут смотреться чужеродно. Сигнатуры SelectMany для IEnunerable такие же страшные. Думаете их кто-то видит и вообще задумывается о них? Многие просто используют готовые инструменты и не вникают. Мне вообще кажется, что скоро мы войдём в эпоху, когда прикладные программисты не будут понимать как Машина выполняет код. Будем писать только DSL.

Найтите 10 различий:


public IActionResult Post(ChangeUserNameCommand command)
        {
            var res = command.Validate();
            if (res.IsFaulted) return res;

            return ChangeUserName(command)
                .OnSuccess(SendEmail)
                .Return<IActionResult>(Ok, x => BadRequest(x.Message));
        }

public IResult Post(ChangeUserNameCommand command)
        {
            command.Validate();

            try {
                return SendEmail(ChangeUserName(command))
            } catch( Exception x ) {
               throw new BadRequest(x.Message);
            }
        }

Давайте я начну:


  1. В первом коде вы узнаете об ошибке где-то далеко от места её возникновения. Во втором отладчик услужливо остановит исполнение там, где она произошла.
Во втором отладчик услужливо остановит исполнение там, где она произошла.

Только если включен отлов First-chance exceptions, но эта настройка зачастую мешает нормальной отладке из-за исключений в библиотеках. В противном случае отладчик остановит исполнение только для тех исключений которые "утекают" из пользовательского кода.


Применительно к вашему коду это означает, что исключение в SendEmail или ChangeUserName поймано отладчиком не будет ни в каком из двух вариантов (а ведь именно эти исключения как правило наиболее интересны — валидация-то зачастую слишком очевидна чтобы ее отлаживать, да и сообщения об ошибках там самые понятные).


По мне, так лучше всего вот так делать:


[HandleErrors]
public IActionResult Post(ChangeUserNameCommand command)
{
      var res = command.Validate();
      if (res.IsFaulted) return Fault(res);

      return SendEmail(ChangeUserName(command));
}

То есть ошибки валидации обрабатываем как возвращаемое значение (это необходимо хотя бы потому что их может быть много, а не одно) — а ошибки обработки запроса надо делать уже исключениями, причем с обязательным утеканием исключений в системный код перед перехватом (тут может помочь [DebuggerNonUserCode] если используемый фреймворк не позволяет обрабатывать исключения какими-нибудь фильтрами).

Более того, сигнатуры методов в языках с исключениями не договаривают.

На самом деле, сигнатуры "не договаривают" в любых языках программирования кроме тех что заточены на доказательство корректности программы. По одной простой причине — в сигнатурах не учитывается тот факт, что функция может вовсе не вернуть управления.


Исключение обычно рассматривается лишь как одна из разновидностей ситуации "функция не вернула управления" (две другие разновидности — завершение процесса и вечный цикл).

«Проблему остановки» кажется же еще не решили:) Я как-то даже не стал уточнять.
Это для Тьюринг-полных языков она неразрешима.
UFO just landed and posted this here
Про ошибки и исключения
Как говорится все зависит.
Исключение бросается при нарушении контракта выполнения. Т.е. в некоторый контекст, который не знает всех деталей прилетают какие-то некорректные данные и тут выброс исключения выглядет логичным.
Если же ошибка возникает в контексте когда можно что-то изменить, то уже бросать исключение нет необходимости, а можно попытаться эту ошибку исправить.

Минус в том, что по сути одни и теже проверки выполняются в разных местах, но это тоже можно обойти или ослабить.
А наворачивание синтаксиса поверху «тупых» вызовов не выглядит как разумным. Универсальность всегда усложняет код, по этому ее надо использовать разумно.
Sign up to leave a comment.

Articles