F#, how far is it reasonable to go when checking for valid arguments?
Asked Answered
E

3

6

I'm trying to learn a little of the mindset of functional programming in F#, so any tips are appreciated. Right now I'm making a simple recursive function which takes a list and returns the i:th element.

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

The function itself seems to work, but it warns me about an incomplete pattern. I'm not sure what to return when I match the empty list in this case, since if I for example do the following:

| ([], _) -> ()

The whole function is treated like a function that takes a unit as argument. I want it to treat is as a polymorphic function.

While I'm at it, I may as well ask how far is reasonable to go to check for valid arguments when designing a function when developing seriously. Should I check for everything, so "misuse" of the function is prevented? In the above example I could for example specify the function to try to access an element in the list that is larger than its size. I hope my question isn't too confusing :)

Emmaemmalee answered 12/11, 2010 at 14:13 Comment(0)
K
9

You can learn a lot about the "usual" library design by looking at the standard F# libraries. There is already a function that does what you want called List.nth, but even if you're implementing this as an exercise, you can check how the function behaves:

> List.nth [ 1 .. 3 ] 10;;
System.ArgumentException: The index was outside the range 
  of elements in the list. Parameter name: index

The function throws System.ArgumentException with some additional information about the exception, so that users can easily find out what went wrong. To implement the same functionality, you can use the invalidArg function:

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

This is probably better than just using failwith which throws a more general exception. When using invalidArg, users can check for a specific type of exceptions.

As kvb noted, another option is to return option 'a. Many standard library functions provide both a version that returns option and a version that throws an exception. For example List.pick and List.tryPick. So, maybe a good design in your case would be to have two functions - nth and tryNth.

Kilocycle answered 12/11, 2010 at 15:25 Comment(0)
P
5

If you want your function to return a meaningful result and to have the same type as it has now, then you have no alternative but to throw an exception in the remaining case. A matching failure will throw an exception, so you don't need to change it, but you may find it preferable to throw an exception with more relevant information:

| _ -> failwith "Invalid list index"

If you expect invalid list indices to be rare, then this is probably good enough. However, another alternative would be to change your function so that it returns an 'a option:

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

This places an additional burden on the caller, who must now explicitly deal with the possibility of failure.

Picayune answered 12/11, 2010 at 14:39 Comment(0)
T
2

Presumably, if taking an empty list is invalid, you're best off just throwing an exception?

Generally the rules for how defensive you should be don't really change from language to language - I always go by the guideline that if it's public be paranoid about validating input, but if it's private code, you can be less strict. (Actually if it's a large project, and it's private code, be a little strict... basically strictness is proportional to the number of developers who might call your code.)

Tubby answered 12/11, 2010 at 14:32 Comment(3)
"strictness is proportional to the number of developers who might call your code" Awesome summary.Introduction
This may sound a tad trite, but you can never go wrong with "rock solid" code. After all, it's not often you can guarantee how your code will be used, or by whom, in the future.Winona
@Daniel, True, but there is a cost associated with this decision, in both development time and maintenance. If every single method were to be this paranoid, you would wind up with a code base that's full of boilerplate, as well as uninspired developers.Anxious

© 2022 - 2024 — McMap. All rights reserved.