F #, насколько разумно идти, проверяя правильность аргументов? - PullRequest
6 голосов
/ 12 ноября 2010

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

let rec nth(list, i) =
    match (list, i) with
    | (x::xs, 0) -> x
    | (x::xs, i) -> nth(xs, i-1)

Кажется, что сама функция работает, но предупреждает меня о неполном шаблоне. Я не уверен, что возвращать, когда я сопоставляю пустой список в этом случае, так как, если я, например, сделаю следующее:

| ([], _) -> ()

Вся функция рассматривается как функция, которая принимает единицу в качестве аргумента. Я хочу, чтобы это рассматривалось как полиморфная функция.

Пока я в этом, я также могу спросить, насколько разумно идти, чтобы проверить правильность аргументов при разработке функции при серьезной разработке. Должен ли я проверять все, чтобы предотвратить «неправильное» использование функции? В приведенном выше примере я мог бы, например, указать функцию, чтобы попытаться получить доступ к элементу в списке, который больше, чем его размер. Надеюсь, мой вопрос не слишком запутанный:)

Ответы [ 4 ]

9 голосов
/ 12 ноября 2010

Вы можете многое узнать об «обычном» дизайне библиотеки, взглянув на стандартные библиотеки F #.Уже есть функция, которая выполняет то, что вы хотите, называется List.nth, но даже если вы реализуете это как упражнение, вы можете проверить, как она ведет себя:с некоторой дополнительной информацией об исключении, чтобы пользователи могли легко узнать, что пошло не так.Для реализации той же функциональности вы можете использовать функцию invalidArg:

| _ -> invalidArg "index" "Index is out of range."

Это, вероятно, лучше, чем просто использование failwith, которое выдает более общее исключение.При использовании invalidArg пользователи могут проверять наличие определенного типа исключений.

Как отмечено в kvb, другой вариант - вернуть option 'a.Многие стандартные библиотечные функции предоставляют как версию, которая возвращает option, так и версию, которая выдает исключение.Например List.pick и List.tryPick.Так что, возможно, в вашем случае хорошим дизайном было бы иметь две функции - nth и tryNth.

5 голосов
/ 12 ноября 2010

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

| _ -> failwith "Invalid list index"

Если вы ожидаете, что недействительные индексы списка будут редкими, то, вероятно, этого достаточно. Однако другой альтернативой было бы изменить вашу функцию так, чтобы она возвращала 'a option:

let rec nth = function
| x::xs, 0 -> Some(x)
| [],_ -> None
| _::xs, i -> nth(xs, i-1)

Это накладывает дополнительное бремя на абонента, который теперь должен явно иметь дело с возможностью сбоя.

2 голосов
/ 12 ноября 2010

Предположительно, если занятие пустого списка недопустимо, лучше всего просто выбросить исключение?

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

0 голосов
/ 14 ноября 2010
let rec nth (list, i) =
    match list, i with
    | x::xs, 0  ->  x
    | x::xs, i  ->  nth(xs, i-1)
    | [],    _  ->  ()

Эта функция действительно будет иметь (нежелательную) подпись, которую вы упомянули:

val nth : unit list * int -> unit

Почему?Посмотрите на правую сторону этих трех ваших правил.Если бы не (), вы не могли бы сказать, какой конкретный тип значения возвращает ваша функция.Но как только вы добавите последнее правило, F # увидит выражение () (которое имеет тип unit) и из этого может вывести тип возврата вашей функции;который больше не является общим.Поскольку любая функция может иметь только один фиксированный тип возвращаемого значения, она затем выводит, что x, xs также включают тип unit, что приводит к подписи выше.

Как уже отмечалось в kvb, вы хотитеиногда возвращать значение, и при приведении пустого списка ввода вы не хотите ничего возвращать ... значит, возвращаемое значение должно быть 'a option (также может быть записано как option<'a> btw.)

let rec nth (list, i) =
    match list, i with
    | x::xs, 0  ->  Some(x)
    | x::xs, i  ->  nth(xs, i-1)   // <-- nth already returns an 'a option,
    | [],    _  ->  None           //     no need to "wrap" it once more

Теперь указанная подпись выглядит правильно:

val nth : 'a list * int -> 'a option

Относительно вашего второго вопроса , я признаю, что не могу ответить на него полностью, потому что я 'Я все еще новичок F #.Однако один совет: если вы возьмете вышеуказанную функцию в правильной форме (универсальная версия, возвращающая 'a option), вы на самом деле не сможете не проверить все возможные возвращаемые значения:

Почему?Потому что, если вы хотите получить фактическое возвращаемое значение (названное x в только что показанном коде), вы должны «извлечь» его, используя блок match:

let result = nth (someList, someIndex)

match result with
| Some(x) -> ...
| None    -> ...

И так как ваши правилавсегда должно быть исчерпывающим (чтобы компилятор не жаловался), вам автоматически придётся добавить правило, которое проверяет возможность None.

Компилятор на самом деле заставит вам также учитывать, чтодолжно произойти в ситуации «ошибки»;Вы не можете забыть это.Вы можете только выбрать, как с этим справиться!

(Конечно, как только вы занимаетесь программированием на .NET и сталкиваетесь с типами, которые могут быть null, все может выглядеть немного иначе,поскольку null не является родным понятием F #.)


Дальнейшее предложение по улучшению: Если вы измените способ, которым ваша nth функция принимает свои аргументы, выполучить возможность частично применить его, что означает, например, что вы можете использовать его с оператором конвейерной обработки:

let rec nth i list =    // <-- swap order of arguments, don't pass them in
    ...                 //     as a tuple but as two separate arguments

Теперь вы можете сделать это:

someList
|> nth someIndex

Или вот это:

let third = nth 2

someList |> third

Если, с другой стороны, ваша функция принимает только кортеж, это не сработает.Так что подумайте, действительно ли вам нужен параметр кортежа: в этом случае он на самом деле ограничивает гибкость, и, более того, «значение» / содержание двух параметров не предполагает, что они всегда должны храниться и отображаться вместе.Поэтому я бы посоветовал не использовать кортеж в этом случае.

...