Comments 36
Не очень понятен профит относительно Try и \/.
Try и \/ без помощи со стороны в некоторых ситуациях просто не работают. Ситуация с циклами и if, который должен приводить к выходу из функции. Кроме того там не хватает сервисных функций для облегчения интеграции с кодом, не поддерживающим их. Кроме того порой требуют больше кода при преобразовании значений и не позволяют без допиливания уточнять и заменять сообщения об ошибках. Т.е. идея описанного здесь Res — это доведения до более-менее удобного состояния этих самых Try и \/.
Try и \/ без помощи со стороны в некоторых ситуациях просто не работают. Можно пример для наглядности?
Пример с fold в статье. «Пример использования Exit может выглядеть так...». fold как и прочие циклические функции из стандартной библиотеки scala нельзя прервать просто средствами самого Try или \/. Для этих целей у меня используется Exit, который удобно интегрирован с Res.
Пример с if. Скажем у вас есть такая функция, использующая Try:
Пример с Res:
Т.е. во-первых мы можем делать больше действий в пределах одного for, во-вторых можем указывать свои сообщения об ошибках.
Пример с if. Скажем у вас есть такая функция, использующая Try:
case class F1(x: Int, y: Int)
case class In(f1: F1, f2: Int)
case class Out(z: Int, f2: Int)
def convF1(x: F1): Try[Int] = ???
def convF2(x: Int): Try[Int] = ???
def fn(x: In): Try[Out] = {
if (x.f1.x < 0 && x.f1.y < 0)
return Failure(new Exception("x < 0 && y < 0"))
for {
z <- convF1(x.f1); if (z > 0)
f2 <- convF2(x.f2)
} yield Out(z, f2)
}
Пример с Res:
case class F1(x: Int, y: Int)
case class In(f1: F1, f2: Int)
case class Out(z: Int, f2: Int)
def convF1(x: F1): Res[Int] = ???
def convF2(x: Int): Res[Int] = ???
def fn(x: In): Res[Out] = for {
_ <- Res(x.f1.x >= 0 || x.f1.y >= 0), "x < 0 && y < 0") //работает как assert; позволяет указать сообщение об ошибке
z <- convF1(x.f1);
_ <- Res(z > 0, "z должен быть больше 0")
f2 <- convF2(x.f2)
} yield Out(z, f2)
Т.е. во-первых мы можем делать больше действий в пределах одного for, во-вторых можем указывать свои сообщения об ошибках.
case class F1(x: Int, y: Int) {
require(x >= 0 && y >= 0, «x and y must be positive»)
}
В конкретном примере, имхо, лучше так.
require(x >= 0 && y >= 0, «x and y must be positive»)
}
В конкретном примере, имхо, лучше так.
Это не совсем верно будет. В данном примере сам класс F1 может хранить любые значения и в других частях программы они допустимы. Но вот в этой функции преобразования есть свои локальные ограничения.
Согласен. Другой вариант:
scala> Try(res3)
res7: scala.util.Try[F1] = Success(F1(-4,3))
scala> res7.filter(x => x.x >=0 && x.y >= 0)
res8: scala.util.Try[F1] = Failure(java.util.NoSuchElementException: Predicate does not hold for F1(-4,3))
При необходимости можно сделать recover и задать собственное исключение.
scala> Try(res3)
res7: scala.util.Try[F1] = Success(F1(-4,3))
scala> res7.filter(x => x.x >=0 && x.y >= 0)
res8: scala.util.Try[F1] = Failure(java.util.NoSuchElementException: Predicate does not hold for F1(-4,3))
При необходимости можно сделать recover и задать собственное исключение.
Ну опять же: указать своё сообщение прямо на месте нельзя; необходимо создавать целое новое исключение, что во-вервых длиннее, во-вторых без доп. телодвижений теряется часть стека вызовов, а в-третьих требует больше памяти и процессорного времени. Кроме того, работа с исключениями всегда характерна тем, что их можно пропустить там, где не ожидаешь.
Для упрощения разве что сделать вспомогательный метод на фильтр, где можно задать своё сообщение. Исключение все равно бросается в контексте, почему он теряется? И опять же исключение в Try — fast fail. Не вижу оверхеда.
p.s. в тему обсуждения _http://fsharpforfunandprofit.com/posts/recipe-part2/
p.s. в тему обсуждения _http://fsharpforfunandprofit.com/posts/recipe-part2/
В том и суть, что для упрощения надо сделать метод, и даже не один. Кроме того в Res к верхнему уровню мы имеем одно исключение чисто для стека вызовов и список сообщений об ошибках, которые могут задаваться не только строками, но и произвольными типами. При использовании Try к верхнему уровню мы имеем в лучшем случае список исключений каждое со своим стеком или одно с нерелевантным стеком.
Ну и на счет того, что всё кидается в контексте. А что будет если случайно контекст забыть. В случае с Try во многих случаях компилятор ничего не скажет, в случае с Res это будет явная ошибка компиляции. Поэтому я считаю важным отделить работу с ошибками, которые можно обработать, от исключений, которые никто, по хорошему, ловить не должен.
Ну и на счет того, что всё кидается в контексте. А что будет если случайно контекст забыть. В случае с Try во многих случаях компилятор ничего не скажет, в случае с Res это будет явная ошибка компиляции. Поэтому я считаю важным отделить работу с ошибками, которые можно обработать, от исключений, которые никто, по хорошему, ловить не должен.
На счет статьи. Трудно её понять без знания F#. Но в закладки добавил, чтобы хоть с основными принципами по разбираться.
В случае fail fast (тем более с собственными исключениями), имхо, стэктрейс не столь важен. Конечно, если это повсеместный подход в проекте. По идее исключения нужны только для обозначения ситуаций, когда контекст покинул область определения функции и это надо обрабатывать в рамках самой функции. Саму же логику строить «прямолинейно», словно всё хорошо. И лишь в конце кейса сделать выбор что сделать с результатом: просто вернуть корректный ответ или вернуть ответ-ошибку, если такая имела место.
Спасибо за краткое объяснение. А может быть действительно переведете как-нибудь статью, и может даже добавите пару примеров на других языках?
Не могу обещать. Труд большой, а с литературным изложением у меня проблемы. Статья больше обзор идеи, чем готовый рецепт. Незнание F# не помеха.
В довесок можно еще две статьи (с дебатами в комментариях) уже на скале привести:
_http://underscore.io/blog/posts/2015/02/13/error-handling-without-throwing-your-hands-up.html
_http://underscore.io/blog/posts/2015/02/23/designing-fail-fast-error-handling.html
В довесок можно еще две статьи (с дебатами в комментариях) уже на скале привести:
_http://underscore.io/blog/posts/2015/02/13/error-handling-without-throwing-your-hands-up.html
_http://underscore.io/blog/posts/2015/02/23/designing-fail-fast-error-handling.html
/Offtop:
>>p.s. в тему обсуждения _http://fsharpforfunandprofit.com/posts/recipe-part2/
Сожет быть переведете? ;)
>>p.s. в тему обсуждения _http://fsharpforfunandprofit.com/posts/recipe-part2/
Сожет быть переведете? ;)
Как-то велосипедисто. И меня смущает, что `\/` и прочие монады пытаются применяться в том же императивном стиле и контексте. Вы когда правильно код монадами пишите с FF-стретегией, никакого cyclomatic complexity с десятками вложенных методов/for-yield'ов не должно быть. Разбивается всё на мелкие чистые composable-функции и помчали.
У себя в проекте накидал implicit обертку над `Future[\/]` с `EitherT` под капотом. Очень удобно. `map/flatMap`, `ensure` есть, все из коробки работает. Что не так — упали в левую ветку с ошибкой, и всё, дальше код не выполнится.
Если хочется собирать кучу ошибок, то есть стейт монады. Как-то так.
Исключения в бизнес-логике — дурной тон, особенно в ФП. Вы таким образом поток выполнения прерываете ненормальным путем.
У себя в проекте накидал implicit обертку над `Future[\/]` с `EitherT` под капотом. Очень удобно. `map/flatMap`, `ensure` есть, все из коробки работает. Что не так — упали в левую ветку с ошибкой, и всё, дальше код не выполнится.
Если хочется собирать кучу ошибок, то есть стейт монады. Как-то так.
Исключения в бизнес-логике — дурной тон, особенно в ФП. Вы таким образом поток выполнения прерываете ненормальным путем.
А как вы справляетесь с циклами? Для каждого цикла создаете рекурсивную функцию с @tailcall оптимизацией?
>Если хочется собирать кучу ошибок, то есть стейт монады. Как-то так.
Можете привести какой-нибудь пример использования? Я о них мало знаю.
>Исключения в бизнес-логике — дурной тон, особенно в ФП. Вы таким образом поток выполнения прерываете ненормальным путем.
Я с этим согласен. Поэтому у себя их использую локально и ограниченно, только для транспортных целей. В этом случае обычно видно, что ничего критического не прерывается.
>Если хочется собирать кучу ошибок, то есть стейт монады. Как-то так.
Можете привести какой-нибудь пример использования? Я о них мало знаю.
>Исключения в бизнес-логике — дурной тон, особенно в ФП. Вы таким образом поток выполнения прерываете ненормальным путем.
Я с этим согласен. Поэтому у себя их использую локально и ограниченно, только для транспортных целей. В этом случае обычно видно, что ничего критического не прерывается.
Да, для цикла рекурсии более чем достаточно. break это уже какой-то goto, только легализованный.
Ясно. Я тоже по началу пытался рекурсию для циклов использовать. Проблема в том, что порой список параметров раздувает на пару строк, что совсем не есть удобно. Кроме того, надо понимать, что @tailcall рекурсия разворачивается в обычный while. Т.е. большинство проблем while вылезут также и в этой рекурсии.
Кстати, на счет goto. Это как говорить, что атомная бомба абсолютное зло. Но на деле она зло, только когда взрывается рядом с тобой. Я это к тому, что ряд сценариев использования goto действительно приводит к спагетиобразному коду, в котором не разберешься, но такие частные случаи как break и continue вряд ли можно в этом обвинить. И есть ряд вполне безопасных сценариев для их использования.
Кстати, на счет goto. Это как говорить, что атомная бомба абсолютное зло. Но на деле она зло, только когда взрывается рядом с тобой. Я это к тому, что ряд сценариев использования goto действительно приводит к спагетиобразному коду, в котором не разберешься, но такие частные случаи как break и continue вряд ли можно в этом обвинить. И есть ряд вполне безопасных сценариев для их использования.
А в Java в блоке catch можно перевозбудить исходное исключение без создания нового? Мы в Delphi пользуемся вложенными try catch ( try except в терминах Delphi), при этом в блоке catch просто припысываем доп. информацию в текст исключения наподобие Exception.Message = 'Ошибка при обработке файла: ' + Exception.Message и перевозбуждаем это же самое исключение на уровень выше. Т.е. у нас исключение будет одно, но с навешенными к нему приписками.
Перевозбудить можно, а вот изменить что-то нельзя. Поэтому и приходится создавать новое.
Не очень понимаю, почему нельзя сделать как-то так:
Работает правильно. В стектрейсе ошибка тоже меняется.
Ошибка с возможностью перезаписи сообщения
public class Test {
public static class MyException extends Exception{
private String message;
public MyException(String message) {
super();
this.message = message;
}
public void setMessage(String message){
this.message = message;
}
public String getMessage(){
return message;
}
}
public static void main(String[] args) {
try {
A();
} catch (MyException e) {
System.out.println(e.getMessage());
}
}
public static void A() throws MyException {
try {
B();
} catch (MyException e) {
System.out.println(e.getMessage());
e.setMessage("Test2");
throw e;
}
}
public static void B() throws MyException {
throw new MyException("Test");
}
}
Работает правильно. В стектрейсе ошибка тоже меняется.
По идее так сделать можно. Здесь скорее вопрос идеологии, состоящий в том, что изменяемые данные за пределами какой-то очень локальной области кода сами по себе уже являются злом. Затрудняют отладку, увеличивают вероятность ошибки. Здесь же предлагается передавать их через всю программу и модифицировать во многих местах, что является, так сказать, дурным тоном. Ну и кроме того, не совсем красиво выглядит тот факт, что поля, которые уже были в исключении и отвечали за обработку сообщения, так никуда и не делись. И теперь у нас есть несколько полей, отвечающих за одно и тоже, но часть из них ничего не делает.
Писал когда-то обработчик ошибок на Objective-C с использованием патерна Chain of responsibility.
stackoverflow.com/questions/31500254/how-to-rewrite-code-to-optionals
Может тут кто-то знает? :)
Может тут кто-то знает? :)
Вот можно как-то так сделать:
в нужном месте.
optional.orElseThrow(NullPointerException::new);
в нужном месте.
а тогда дальше уже не Optional будет, а голое значение, которое опять заворачивать для продолжения цепочки(а они длинные бывают, это я для примера укротил)
Тут в любом случае будет некий костыль, так как идея Optional заключалась в том, что бы избавится от npe, а не удобно ими управлять.
Есть еще вариант с условием:
Сомневаюсь, что есть что-то другое. Разве что переписывать Option или дополнять его.
Есть еще вариант с условием:
if (!optional.isPresent()){
throw new NullPointerException();
}
Сомневаюсь, что есть что-то другое. Разве что переписывать Option или дополнять его.
идеально было бы что-то вроде
mapStrict(mapfunc)
/mapStrict(mapfunc, errorfunc)
, который получив на входе не null, а на выходе null бросал бы nre, и иначе вовращал бы классический optional. и реализация в принципе-то не сильно сложна, да вот интересно, может можно что-то такое сделать с помощью штатных средств…Можно засунуть выброс npe в сам map
Как-то так:
Но только этот код не вызовет npe, если el уже был null
Как-то так:
optional.map(el->{
if (el.getA()==null){
throw new NullPointerException();
}
return el.getA();
});
Но только этот код не вызовет npe, если el уже был null
такой подход нарушает концепцию монады, сама применяемая к монаде функция не должна следить за контекстом, это должна делать монада.
грубо говоря это что-то вроде такого:
То есть компилятор так писать не запретит, но делать так очень плохо.
грубо говоря это что-то вроде такого:
public int getCount()
{
if(this.parent.a < 0)
this.parent.a = 42;
return count;
}
То есть компилятор так писать не запретит, но делать так очень плохо.
Ну если на scala, то for сам и разворачивает значения и заворачивает обратно. А вот на чистой java с этим всё достаточно печально.
А если вот так? Тогда логика отдельно, обработка ошибок отдельно.
def convertPerson(p: InPerson): OutPerson = {
try {
val name = convertName(p.name)
val homeAddr = convertAddr(p.addr)
val age = convertAge(p.age)
val (id, wAddr) = db.query("select id, work_addr from persons where tel = ?", p.tel)
val workAddr = addrFromStr(wAddr)
val distance = calcAndCheckDistance(p, homeAddr, workAddr)
OutPerson(id, name, homeAddr, workAddr, distance)
} catch {
case e @ _: AddressException | _: AgeException => throw new PersonException(s"ошибка при обработке человека ${p.name}", p, e)
case e: NotFoundException => throw new PersonException(s"в базе нет данных о человеке с номером телефона ${p.tel}", p, e)
case e: ZeroDistanceExceptin => throw new PersonException(s"похоже, что человек ${p.name} живет на работе", p, e)
}
}
def calcAndCheckDistance(p: InPerson, homeAddr: Nothing, workAddr: Nothing) = {
val distance = calcDistance(homeAddr, workAddr)
if (distance == 0)
throw new ZeroDistanceExceptin()
distance
}
Есть ряд причин, по которым из двух зол (исключения и монады), я бы всегда выбирал монады.
При работе с исключениями в текущем их виде мне не нравятся следующие вещи:
— по заголовку функции может быть не понятно, что функция кидает исключение;
— если ты его не отловил, то компилятор ничего не скажет;
— если ты его не отловил, то его действие распространяется на вышестоящие функции и может привести к некорректному закрытия каких-то ресурсов и повреждению данных;
— если же ты его всё-таки отлавливаешь, то часто тонешь в избыточном синтаксисе try...catch (да и throw new многовато для такой фундаментальной операции, как возврат ошибки)
Монады частично решают эти проблемы:
— из заголовка явно видно, что функция может завершиться с ошибкой;
— есть оператор for, который позволяет устранить избыточность синтаксиса;
— если вышестоящая функция явно не рассчитана на работу с ошибками нижестоящей функции, то это вызовет ошибку компиляции в большинстве случаев;
— действие монад всегда локально и не затрагивает вышестоящие функции;
При работе с исключениями в текущем их виде мне не нравятся следующие вещи:
— по заголовку функции может быть не понятно, что функция кидает исключение;
— если ты его не отловил, то компилятор ничего не скажет;
— если ты его не отловил, то его действие распространяется на вышестоящие функции и может привести к некорректному закрытия каких-то ресурсов и повреждению данных;
— если же ты его всё-таки отлавливаешь, то часто тонешь в избыточном синтаксисе try...catch (да и throw new многовато для такой фундаментальной операции, как возврат ошибки)
Монады частично решают эти проблемы:
— из заголовка явно видно, что функция может завершиться с ошибкой;
— есть оператор for, который позволяет устранить избыточность синтаксиса;
— если вышестоящая функция явно не рассчитана на работу с ошибками нижестоящей функции, то это вызовет ошибку компиляции в большинстве случаев;
— действие монад всегда локально и не затрагивает вышестоящие функции;
— по заголовку функции может быть не понятно, что функция кидает исключение;
— если ты его не отловил, то компилятор ничего не скажет;
— если ты его не отловил, то его действие распространяется на вышестоящие функции и может привести к некорректному закрытия каких-то ресурсов и повреждению данных;
Вот мы и пришли к необходимости throws у метода, как в Java :)
Sign up to leave a comment.
Размышления о способах обработки ошибок