F #, how reasonable is it to go when checking for valid arguments?

I am trying to learn a little about functional programming in F #, so any advice is appreciated. Right now I'm doing a simple recursive function that takes a list and returns an 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 works, but it warns me of an incomplete template. I am not sure what to return when I match an empty list in this case, since if, for example, I do the following:

 | ([], _) -> () 

The whole function is considered as a function that takes one as an argument. I want it to be considered as a polymorphic function.

While I am in this, I can ask how reasonable it is to go to check the valid arguments when developing a function with serious development. Do I have to check everything, so the "misuse" of the function is prevented? In the example above, I could, for example, specify a function to try to access an item in a list that is larger than its size. Hope my question is not too confusing: :)

+6
pattern-matching arguments f # unit-type
source share
4 answers

You can learn a lot about the “regular” library design by looking at the standard F # libraries. There is already a function that does what you want to call List.nth , but even if you implement it as an exercise, you can check how the function works:

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

The function throws a System.ArgumentException with 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 certain types of exceptions.

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

+9
source share

If you want your function to return a meaningful result and have the same type as it is now, then you have no alternative but to throw an exception in the remaining case. The corresponding rejection throws an exception, so you do not need to modify it, but you may find it preferable to throw an exception with more relevant information:

 | _ -> failwith "Invalid list index" 

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

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

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

+5
source share

Presumably if accepting an empty list is invalid, would you be best off throwing an exception?

As a rule, the rules on how to protect you should not really change from language to language - I always adhere to the instruction that if it is publicly paranoid regarding input verification, but if it is a private code, you may be less strict. (In fact, if this is a large project and its private code, be a little strict ... basically rigor is proportional to the number of developers who can name your code.)

+2
source share
 let rec nth (list, i) = match list, i with | x::xs, 0 -> x | x::xs, i -> nth(xs, i-1) | [], _ -> () 

This function will indeed have the (unwanted) signature you mentioned:

 val nth : unit list * int -> unit 

Why? Look at the right side of these three rules. If this is not for () , you cannot say what specific type of value your function returns. But as soon as you add the last rule, F # sees the expression () (which has a unit type), and from it you can infer the return type of your function; which is now no longer common. Since any function can have only one fixed return type, it infers that x , xs also includes the unit type, with the result that the signature is higher.

As kvb already noted, you want to sometimes return a value, but in the sheet of an empty input list you don’t want to return anything ... that is, your return value should be 'a option (can also be written as 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 

Now the reporting signature looks right:

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

Regarding your second question , I admit that I cannot fully answer it because I'm still a F # newbie. One hint: if you take the above function in the correct form (the general version returns 'a option ), you actually cannot help but check all the possible return values:

Why? Since, if you want to get the actual return value (with the name x code just shown), you need to "extract" it using the match block:

 let result = nth (someList, someIndex) match result with | Some(x) -> ... | None -> ... 

And since your rules should always be comprehensive (so that the compiler does not complain), a rule will automatically be added to you that checks the possibility of None .

The compiler will actually make you also think about what should happen in a “bug” situation; you cannot forget about it. You just choose how to handle it!

(Of course, once you program .NET and deal with types that may be null , things may look a little different, since null not a native concept of F #.)


Further suggestion for improvement:. If you change the way your nth function takes its arguments, you get the opportunity to partially apply it, which means, for example, that you can use it with the pipeline operator:

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

Now you can do this:

 someList |> nth someIndex 

Or that:

 let third = nth 2 someList |> third 

If, on the other hand, your function only accepts a tuple that will not work. Therefore, consider whether you really need the tuple parameter: in this case, it actually limits the flexibility, and, moreover, the "meaning" / content of the two parameters does not imply that they should always be stored and displayed together. Therefore, I would advise against using a tuple in this case.

0
source share

All Articles