Обработка исключений в конвейерной последовательности - PullRequest
6 голосов
/ 24 ноября 2011

Я работаю над базовым движком 2D CAD, и оператор трубопровода значительно улучшил мой код. В основном несколько функций начинаются с точки (x, y) в пространстве и вычисляют конечную позицию после ряда операций перемещения:

let finalPosition =
    startingPosition
    |> moveByLengthAndAngle x1 a1 
    |> moveByXandY x2 y2
    |> moveByXandAngle x3 a3
    |> moveByLengthAndAngle x4 a4
    // etc...

Это невероятно легко читать, и я бы хотел, чтобы это так и оставалось. Различные x1, a1 и т. Д., Очевидно, имеют значение в реальном коде.

Теперь новое требование - ввести обработку исключений. Недостаточно больших попыток во всей цепочке операций, потому что я хотел бы знать, какая строка вызвала исключение. Мне нужно знать, какой аргумент недопустим, чтобы пользователь знал, какой параметр необходимо изменить.

Например, если первая строка (moveByLengthAndAngle x1 a1) вызывает исключение, я хотел бы сказать что-то вроде «Эй, -90 - недопустимое значение для a1! A1 должно быть между 45 и 90!». Учитывая, что в последовательности можно использовать много операций одного и того же типа, недостаточно определить разные типы исключений для каждой операции (в этом примере я не смог бы определить, была ли ошибка первым или последним перемещением).

Очевидное решение состоит в том, чтобы разбить цепочку на отдельные операторы let, каждое в пределах соответствующей попытки / с. Это, однако, сделало бы мой красивый и читаемый код немного грязным, уже не так читаемым.

Есть ли способ удовлетворить это требование, не жертвуя удобочитаемостью и элегантностью текущего кода?

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

Ответы [ 5 ]

5 голосов
/ 25 ноября 2011

Как насчет сворачивания по выбору? Допустим, что вместо конвейеризации действий вы представляете их следующим образом:

let startingPosition = 0. ,0.

let moveByLengthAndAngle l a (x,y) = x,y // too lazy to do the math
let moveByXandY dx dy (x,y) = 
    //failwith "oops"
    x+dx, y+dy
let moveByXandAngle dx a (x,y) = x+dx, y

let actions = 
    [
        moveByLengthAndAngle 0. 0., "failed first moveByLengthAndAngle"
        moveByXandY 1. 2., "failed moveByXandY"
        moveByXandY 3. 4., "failed moveByXandY"
        moveByXandAngle 3. 4., "failed moveByXandAngle"
        moveByLengthAndAngle 4. 5., "failed second moveByLengthAndAngle"
    ]

т.е. actions имеет тип ((float * float -> float * float) * string) list.

Теперь, используя FSharpx , мы поднимаем действия на Выбор и сворачиваем / связываем ( не уверен, как его назвать это похоже на foldM в Haskell ) за действия:

let folder position (f,message) =
    Choice.bind (Choice.protect f >> Choice.mapSecond (konst message)) position

let finalPosition = List.fold folder (Choice1Of2 startingPosition) actions

finalPosition имеет тип Choice<float * float, string>, т. Е. Это либо конечный результат всех этих функций, либо ошибка (как определено в приведенной выше таблице).

Объяснение этого последнего фрагмента:

  • Choice.protect аналогичен защите Томаса, за исключением того, что когда он находит исключение, он возвращает исключение, заключенное в Choice2Of2. Когда нет исключения, он возвращает результат, заключенный в Choice1Of2.
  • Choice.mapSecond изменяет это потенциальное исключение в Choice2Of2 с сообщением об ошибке, определенным в таблице действий. Вместо (сообщения konst) это также может быть функция, которая создает сообщение об ошибке с использованием исключения.
  • Choice.bind запускает это «защищенное» действие против текущей позиции. Он не запустит фактическое действие, если текущая позиция ошибочна (то есть, Choice2Of2).
  • Наконец, складка применяет все действия, продвигающиеся / накапливающие полученный выбор (либо текущая позиция, либо ошибка).

Так что теперь нам просто нужно сопоставить шаблон для обработки каждого случая (правильный результат или ошибка):

match finalPosition with
| Choice1Of2 (x,y) -> 
    printfn "final position: %f,%f" x y
| Choice2Of2 error -> 
    printfn "error: %s" error

Если вы раскомментируете failwith "oops" выше, finalPosition будет Choice2Of2 "failed moveByXandY"

5 голосов
/ 25 ноября 2011

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

Например, допустим, у вас есть следующие простые функции:

let times2 n = n * 2
let plus a b = a + b
let fail n = failwith "inside fail"

10 // This will handle exception that happens when evaluating arguments
   |> try plus (failwith "evaluating args") with _ -> 0 
   |> times2                                            
   |> try fail with _ -> 0 // This will not handle the exception from 'fail'!

Чтобы решить эту проблему, вы можете написать функцию, которая оборачивает любую другую функцию в обработчик исключений.Идея в том, что ваша функция protect будет принимать функцию (например, times2 или fail) и будет возвращать новую функцию, которая принимает входные данные из конвейера (число) и передает их функции (times2 илиfail), но будет делать это внутри обработчика исключений:

let protect msg f = 
  fun n -> 
    try
      f n 
    with _ ->
      // Report error and return 0 to the pipeline (do something smarter here!)
      printfn "Error %s" msg
      0

Теперь вы можете защитить каждую функцию в конвейере, и она также будет обрабатывать исключения, возникающие при оценке этих функций:

let n =
  10 |> protect "Times" times2
     |> protect "Fail" fail
     |> protect "Plus" (plus 5)
5 голосов
/ 24 ноября 2011

Существует множество способов достичь этого, проще всего было бы просто обернуть каждый вызов в блок try-with:

let finalPosition =
    startingPosition
    |> (fun p -> try moveByLengthAndAngle x1 a1 p with ex -> failwith "failed moveByLengthAndAngle")
    |> (fun p -> try moveByXandY x2 y2 p with ex -> failwith "failed moveByXandY")
    |> (fun p -> try moveByXandAngle x3 a3 p with ex -> failwith "failed moveByXandAngle")
    |> (fun p -> try moveByLengthAndAngle x4 a4 p with ex -> failwith "failed moveByLengthAndAngle")
    // etc...

Вот сила выражения, ориентированного программирования:).

К сожалению, если вы конвейеризуете последовательность, это становится намного сложнее:

  1. В конвейере (для Seqs) происходит компоновка, а не выполнение.
  2. Обработка исключений внутри IEnumerable не определена и зависит от реализации перечислителя.

Единственный безопасный способ - убедиться, что внутренности каждой операции последовательности обернуты.

Edit: Wow я не могу поверить, что я все испортил. Сейчас это исправлено, но я думаю, что два других решения более чистые.

2 голосов
/ 24 ноября 2011

Мне непонятно, почему

Теперь новое требование - ввести обработку исключений. Большой попробуйте / вокруг всей цепочки операций не достаточно, потому что я бы хотелось бы узнать, какая строка вызвала исключение. Мне нужно знать какой аргумент недействителен, так что пользователь знает, какой параметр должен быть Измененное.

для этого отладчика недостаточно. Это звучит как ошибка времени разработки в коде пользователя; каждый из этих методов может выдавать ArgumentException, и ничто не сможет его обработать (это приведет к аварийному завершению приложения), а программист отладит и увидит метод / стек, вызвавший исключение, а текст исключения будет иметь имя аргумента.

(А может это обычно FSI / скриптинг?)

0 голосов
/ 24 ноября 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...