Pull to refresh

Comments 36

Не очень понятен профит относительно Try и \/.
Try и \/ без помощи со стороны в некоторых ситуациях просто не работают. Ситуация с циклами и if, который должен приводить к выходу из функции. Кроме того там не хватает сервисных функций для облегчения интеграции с кодом, не поддерживающим их. Кроме того порой требуют больше кода при преобразовании значений и не позволяют без допиливания уточнять и заменять сообщения об ошибках. Т.е. идея описанного здесь Res — это доведения до более-менее удобного состояния этих самых Try и \/.
Try и \/ без помощи со стороны в некоторых ситуациях просто не работают. Можно пример для наглядности?
Пример с fold в статье. «Пример использования Exit может выглядеть так...». fold как и прочие циклические функции из стандартной библиотеки scala нельзя прервать просто средствами самого Try или \/. Для этих целей у меня используется Exit, который удобно интегрирован с Res.

Пример с 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»)
}
В конкретном примере, имхо, лучше так.
Это не совсем верно будет. В данном примере сам класс 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 и задать собственное исключение.
Ну опять же: указать своё сообщение прямо на месте нельзя; необходимо создавать целое новое исключение, что во-вервых длиннее, во-вторых без доп. телодвижений теряется часть стека вызовов, а в-третьих требует больше памяти и процессорного времени. Кроме того, работа с исключениями всегда характерна тем, что их можно пропустить там, где не ожидаешь.
Для упрощения разве что сделать вспомогательный метод на фильтр, где можно задать своё сообщение. Исключение все равно бросается в контексте, почему он теряется? И опять же исключение в Try — fast fail. Не вижу оверхеда.

p.s. в тему обсуждения _http://fsharpforfunandprofit.com/posts/recipe-part2/
В том и суть, что для упрощения надо сделать метод, и даже не один. Кроме того в Res к верхнему уровню мы имеем одно исключение чисто для стека вызовов и список сообщений об ошибках, которые могут задаваться не только строками, но и произвольными типами. При использовании Try к верхнему уровню мы имеем в лучшем случае список исключений каждое со своим стеком или одно с нерелевантным стеком.
Ну и на счет того, что всё кидается в контексте. А что будет если случайно контекст забыть. В случае с 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
/Offtop:
>>p.s. в тему обсуждения _http://fsharpforfunandprofit.com/posts/recipe-part2/
Сожет быть переведете? ;)
Как-то велосипедисто. И меня смущает, что `\/` и прочие монады пытаются применяться в том же императивном стиле и контексте. Вы когда правильно код монадами пишите с FF-стретегией, никакого cyclomatic complexity с десятками вложенных методов/for-yield'ов не должно быть. Разбивается всё на мелкие чистые composable-функции и помчали.

У себя в проекте накидал implicit обертку над `Future[\/]` с `EitherT` под капотом. Очень удобно. `map/flatMap`, `ensure` есть, все из коробки работает. Что не так — упали в левую ветку с ошибкой, и всё, дальше код не выполнится.

Если хочется собирать кучу ошибок, то есть стейт монады. Как-то так.

Исключения в бизнес-логике — дурной тон, особенно в ФП. Вы таким образом поток выполнения прерываете ненормальным путем.
А как вы справляетесь с циклами? Для каждого цикла создаете рекурсивную функцию с @tailcall оптимизацией?

>Если хочется собирать кучу ошибок, то есть стейт монады. Как-то так.
Можете привести какой-нибудь пример использования? Я о них мало знаю.

>Исключения в бизнес-логике — дурной тон, особенно в ФП. Вы таким образом поток выполнения прерываете ненормальным путем.
Я с этим согласен. Поэтому у себя их использую локально и ограниченно, только для транспортных целей. В этом случае обычно видно, что ничего критического не прерывается.
Да, для цикла рекурсии более чем достаточно. break это уже какой-то goto, только легализованный.
Ясно. Я тоже по началу пытался рекурсию для циклов использовать. Проблема в том, что порой список параметров раздувает на пару строк, что совсем не есть удобно. Кроме того, надо понимать, что @tailcall рекурсия разворачивается в обычный while. Т.е. большинство проблем while вылезут также и в этой рекурсии.

Кстати, на счет 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.
Вот можно как-то так сделать:
optional.orElseThrow(NullPointerException::new);

в нужном месте.
а тогда дальше уже не Optional будет, а голое значение, которое опять заворачивать для продолжения цепочки(а они длинные бывают, это я для примера укротил)
Тут в любом случае будет некий костыль, так как идея Optional заключалась в том, что бы избавится от npe, а не удобно ими управлять.
Есть еще вариант с условием:
if (!optional.isPresent()){
    throw new NullPointerException();
}


Сомневаюсь, что есть что-то другое. Разве что переписывать Option или дополнять его.
идеально было бы что-то вроде mapStrict(mapfunc)/mapStrict(mapfunc, errorfunc), который получив на входе не null, а на выходе null бросал бы nre, и иначе вовращал бы классический optional. и реализация в принципе-то не сильно сложна, да вот интересно, может можно что-то такое сделать с помощью штатных средств…
Можно засунуть выброс npe в сам map
Как-то так:
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, который позволяет устранить избыточность синтаксиса;
— если вышестоящая функция явно не рассчитана на работу с ошибками нижестоящей функции, то это вызовет ошибку компиляции в большинстве случаев;
— действие монад всегда локально и не затрагивает вышестоящие функции;
— по заголовку функции может быть не понятно, что функция кидает исключение;
— если ты его не отловил, то компилятор ничего не скажет;
— если ты его не отловил, то его действие распространяется на вышестоящие функции и может привести к некорректному закрытия каких-то ресурсов и повреждению данных;

Вот мы и пришли к необходимости throws у метода, как в Java :)
Ну в принципе да. Сама идея явного описания ошибок в заголовке мне нравится. Но с текущей реализацией в Java работать просто невозможно, т.к. на 10 строк бизнесс-логики будет 50 строк обработки исключений.
Sign up to leave a comment.

Articles