Functions versus member methods in F#

F# is a hybrid object-functional language, and allows you to write code in member methods (like C# methods) or in global functions. The F# library contains several cases where a member and a global function do the same thing. For example:

> let l = [ 1; 2; 3 ];;
val l : int list = [1; 2; 3]
> l.Length;;
val it : int = 3
> List.length l;;
val it : int = 3

Several other List module functions, such as head and tail, are also duplicated by properties. It’s not just lists, either: for example, the .NET String.Length property is duplicated by the F# library String.length module function.

So when would you choose a member such as .Length member over a function such as List.length or String.length, or vice versa?

The answer, perhaps surprisingly, is that you should usually choose the function. The reason is to do with F# type inference.

In F# code, you don’t usually need to specify the types of variables and parameters, because the compiler will work them out by analysing the code. Take a look at the following code fragment:

let twiceLength a = 2 * List.length a

F# infers that twiceLength takes a list and returns an int. We’ve not had to specify this: F# has worked it out. How?

Well, when F# tries to work out the type of a, it looks at how a is used in the body of the function. It sees that a is passed to the List.length function. Now it looks at the List.length function and sees that its argument is of list type. So, F# reasons, a must be of list type. Job done!

But what if we wrote the function using a member?

let twiceLength a = 2 * a.Length

Oh no! We get a compiler error, “Lookup on object of indeterminate type based on information prior to this program point.” What does this mean? It means that F# can only figure out that a must be something with a .Length member. And that’s not enough to pin down the type. List has a .Length member, String has a .Length member, ExperimentalGermanFilm has a .Length member… F# can’t tell which of these is intended, so automatic type inference fails, and we have to go back to writing it out longhand:

let twiceLength (a : String) = 2 * a.Length  // Now F# can resolve the .Length call

In terms of the amount of code, there isn’t much to choose between the two. And of course, if you’re working with a type that only comes with members, not helper functions, then you’ll have to go the type annotation route — not that there’s anything wrong with that. But type inference is a bit more idiomatic where you have a choice.

And that, my liege, is how we know the earth to be banana shaped.

Tagged as F#

Leave a Reply


Join our mailer

You should join our newsletter! Sent monthly:

Back to Top