Comments 21
Зачем пытаться из C# сделать F#?
Тем более этот SQL синтаксис
from x in xs select x
выглядит чужеродно для не коллекций.
И про исключения очередные мифы дядюшки Римуса.
Что конкретно вы считаете «мифом»?
Все типовые доводы против исключений в вашей статье.
Возьмем один самых распространенных и вредных:
ошибки – объекты первого класса
Нет. Объекты первого класса — данные, ошибки — просчеты при работе с данными, дефекты — разница между неверными и правильными данными, сбой — результат обработки неверных данных без коррекции.
Это частный случай более общего мифа о равноценности отработки основного алгоритма и обработке сбоев.
На деле же все наоборот:
- Если не реализован основной алгоритм — у вас вообще ничего нет, никакого продукта и никакой ценности не создано. Про обработку ошибок речь даже не заходит.
- Если не реализована обработка ошибок — вы несете убытки при сбоях. Это может быть очень неприятно, но программа без обработки ошибок все равно имеет определенную ценность. Более того, в львиной доле вариантов использования этого вполне достаточно.
- Сложность основного алгоритма в общем случае — существенная. Сложность обработки ошибок даже в самых запутанных случаях — акцидентная по Бруксу.
ошибки – объекты первого класса
Вы вырвали из контекста. Полная фраза такая:
Является ли любая ошибка «исключительной ситуацией»? Если вы когда-нибудь сталкивались с бухгалтерским или налоговым учетом, то наверняка знаете, что существует специальный термин «корректировка». Он означает, что в прошлом отчетном периоде были поданы неверные сведения и их необходимо исправить. То есть в сфере учета, без которой бизнес не может существовать в принципе, ошибки – объекты первого класса. Для них введены специальные термины. Можно ли назвать их исключительными ситуациями? Нет. Это нормальное поведение. Люди ошибаются.
В налоговом и бух.учетах «корректировки» — такие же участники предметной области как и «отчетный период» и «подаваемые сведения». Более того, если налоговая находит ошибки в отчетности, то вы обязаны подать корректировку и оплатить штраф. Т.е. для обработки ошибок есть отдельный бизнес-процесс. Это не миф, это реальность в странах, где существует налоговая система, коих большинство.
Если не реализована обработка ошибок — вы несете убытки при сбоях. Это может быть очень неприятно, но программа без обработки ошибок все равно имеет определенную ценность. Более того, в львиной доле вариантов использования этого вполне достаточно
Knight capital потеряла $440.000.000 за 30 минут из-за ошибки. Да она была не в программе, а из-за человеческого фактора при выкладке. Кто знает, если бы в их ПО была система самодиагностики оно бы аварийно завершилось и компания понесла бы убытки, но не обанкротилась. Один из наших клиентов занимается разработкой медицинских тренажеров. Стоимость ошибок в их отрасли — человеческие жизни. «Львиная доля» — понятие весьма субъективное. Если в сфере, где вы работает нужно, чтобы программа «делала вид, что как-то работает», это не значит, что критерии успешности других людей совпадают с вашими.
Сложность основного алгоритма в общем случае — существенная. Сложность обработки ошибок даже в самых запутанных случаях — акцидентная по Бруксу.
Если бы эта сложнасть была «акцидентная», Брукс не предлагал бы тратить половину (Карл!) времени на системное тестирование и отладку. Да не все это время уйдет на исправлние именно ошибок. Очень много «съест» работа с изменениями требований. Ноги этой проблемы все-равно растут из нежелания разбираться с «побочными» путями выполнения программы: тут не учли, здесь забыли, тут доработочка. Сдвинем ка релиз на пол года.
Try(() => File.Create(«abc»)).Correct(() => File.Create(«dfg»)).Bind(p => p.Write(a)).AndAlso(p => p.Close);
Сначала вводим тип-объединение, чтобы заставить вызывающий код проверять ошибки. А потом используем слово на букву М, чтобы их тупо игнорировать. Что-то тут не так..
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);
}
}
Давайте я начну:
- В первом коде вы узнаете об ошибке где-то далеко от места её возникновения. Во втором отладчик услужливо остановит исполнение там, где она произошла.
Во втором отладчик услужливо остановит исполнение там, где она произошла.
Только если включен отлов 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]
если используемый фреймворк не позволяет обрабатывать исключения какими-нибудь фильтрами).
Более того, сигнатуры методов в языках с исключениями не договаривают.
На самом деле, сигнатуры "не договаривают" в любых языках программирования кроме тех что заточены на доказательство корректности программы. По одной простой причине — в сигнатурах не учитывается тот факт, что функция может вовсе не вернуть управления.
Исключение обычно рассматривается лишь как одна из разновидностей ситуации "функция не вернула управления" (две другие разновидности — завершение процесса и вечный цикл).
Про ошибки и исключенияКак говорится все зависит.
Исключение бросается при нарушении контракта выполнения. Т.е. в некоторый контекст, который не знает всех деталей прилетают какие-то некорректные данные и тут выброс исключения выглядет логичным.
Если же ошибка возникает в контексте когда можно что-то изменить, то уже бросать исключение нет необходимости, а можно попытаться эту ошибку исправить.
Минус в том, что по сути одни и теже проверки выполняются в разных местах, но это тоже можно обойти или ослабить.
А наворачивание синтаксиса поверху «тупых» вызовов не выглядит как разумным. Универсальность всегда усложняет код, по этому ее надо использовать разумно.
Про ошибки и исключения