F #: Некоторые, Нет или Исключение?

Я учил себя F # в последнее время, и я исхожу из императивного (С++/С#) фона. В качестве упражнения я работал над функциями, которые могут делать вещи с помощью матриц, например, добавлять, умножать, получать детерминанты и т.д. В этом отношении все идет хорошо, но я считаю, что, возможно, я не принимаю наилучших решений, когда это касается обработки неверные входы, например:

// I want to multiply two matrices
let mult m1 m2 =
  let sizeOK = validateDims m1 m2

  // Here is where I am running to conceptual trouble:
  // In a C# world, I would throw an exception.
  if !sizeOK then
    raise (InvalidOperationException("bad dimensions!")
  else
    doWork m1 m2  

Итак, пока это технически работает, подходит ли это для функционального языка? Это в духе функционального программирования? Или было бы более целесообразно переписать его как:

let mult m1 m2 =
  let sizeOK = validateDims m1 m2

  if !sizeOK then
    None
  else
    Some doWork m1 m2  

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

Ответ 1

Я стараюсь избегать исключений по следующим причинам:

  • Исключения .NET медленные
  • Исключения меняют управление потоками программ неожиданным образом, что затрудняет рассуждение о
  • Исключения часто возникают в критических ситуациях, в то время как вы можете отказоустойчиво, используя параметры.

В вашем случае я буду следовать правилам библиотеки основной библиотеки F # (например, List.tryFind и List.find и т.д.) и создать обе версии:

let tryMult m1 m2 =
  let sizeOK = validateDims m1 m2

  if not sizeOK then
    None
  else
    Some <| doWork m1 m2

let mult m1 m2 =
  let sizeOK = validateDims m1 m2

  if not sizeOK then
    raise <| InvalidOperationException("bad dimensions!")
  else
    doWork m1 m2 

Этот пример не является исключительным для использования исключений. Функция mult включена для совместимости с С#. Кто-то, использующий вашу библиотеку в С#, не имеет шаблонов для быстрого разложения параметров.

Один из недостатков опций заключается в том, что они не дают причины, по которой функция не создала значение. Это переполняет здесь; обычно Choice (или любая монада в терминах Haskell) более подходит для обработка ошибок:

let tryMult m1 m2 =
  // Assume that you need to validate input
  if not (validateInput m1) || not (validateInput m2) then
     Choice2Of2 <| ArgumentException("bad argument!")
  elif not <| validateDims m1 m2 then
    Choice2Of2 <| InvalidOperationException("bad dimensions!")
  else
    Choice1Of2 <| doWork m1 m2

Жаль, что в F # Core отсутствуют функции высокого порядка, чтобы манипулировать выбором. Вы можете найти эти функции в FSharpX или ExtCore.

Ответ 2

Я придерживаюсь следующих рекомендаций:

Использовать исключение в функции, которая, как предполагается, всегда имеет возвращаемые значения, когда что-то пойдет не так неожиданно. Это может быть, например, если аргументы не подчиняются контракту для функции. Это имеет то преимущество, что клиентский код упрощается.

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

Ваше дело несколько в промежутке. Если вы ожидаете, что в первую очередь будут использоваться в местах, где размеры действительны, я бы выбрал исключение. Если вы ожидаете, что код клиента часто вызывает его с недопустимым измерением, я бы вернул опцию. Я, вероятно, поеду с первым, так как он чище (см. Ниже), но я не знаю вашего контекста:

// With exception
let mult3 a b c = 
  mult (mult a b) c;

// With option
let mult3 a b c= 
   let option = mult a b
   match option with
     | Some(x) -> mult x b
     | None -> None

Отказ от ответственности: у меня нет профессионального опыта работы с функциональным программированием, но я программирование на F # на уровне выпускников.

Ответ 3

Мне нравятся вышеупомянутые ответы, но я хотел добавить еще один вариант. Это действительно зависит от неожиданного результата и имеет ли смысл действовать. Если это редкое событие и вызывающий объект, вероятно, не планировали провалиться, то исключение является вполне респектабельным. Код, чтобы поймать исключение, может быть много уровней выше, и вызывающий абонент, вероятно, не планировал сбой. Если это действительно рутинный результат для операции сбой, Some/None одобрен, хотя он дает вам только два варианта и никакой возможности передать результат. Другой вариант - сделать дискриминированный союз возможностей. Это приводит к тому, что вызывающий объект может соответствовать различным результатам, является расширяемым и не заставляет вас делать каждый результат одним и тем же типом данных.

например.

type MultOutcome =
    | RESULT of Matrix
    | DIMERROR 
    | FOOERROR of string


let mult a b =
    if dimensionsWrong then
        DIMERROR
    elif somethingElseIDoNotLike then
        FOOERROR("specific message")
    else
        DIMRESULT(a*b)


match mult x y with
    | DIMERROR ->  printfn "I guess I screwed up my matricies"
    | FOOERROR(s) -> printfn "Operation failed with message %s" s
    | DIMRESULT(r) ->
         // Proceed with result r