F# Language Details (Gotchas)
The ‘F’ in F# stands for fun. However, there are some details in F# that might lead to bugs, surprises, and/or un-fun. This post is to highlight a couple of random ‘gotchas’ when exploring some corners of the F# language.
Overriding Equals and Not Equals
F# allows you to overload operators so you can better map mathematical concepts to your code. For example, consider this simple vector type. Notice how it overrides the Equals operator to compare just the vector’s magnitudes, not their actual values.
type Vector = | Vector of float * float * float member this.Length = // Extract values via ninja pattern match // hidden in a let binding let (Vector(x,y,z)) = this sqrt <| x * x + y * y + z * z static member (+) (lhs, rhs) = let (Vector(x1,y1,z1)) = lhs let (Vector(x2,y2,z2)) = rhs Vector(x1 + x2, y1 + y2, z1 + z2) // Equals static member (==) (lhs : Vector, rhs : Vector) = lhs.Length = rhs.Length // Not equals static member (!=) (lhs : Vector, rhs) = not (lhs == rhs)
When you run this code in FSI, it doesn’t work.
> // Bug? let vec1 = Vector(10.0, 0.0, 0.0) let vec2 = Vector(0.0, 0.0, 10.0);; val vec1 : Vector = Vector (10.0,0.0,0.0) val vec2 : Vector = Vector (0.0,0.0,10.0) > vec1 = vec2;; val it : bool = false > vec1.Length = vec2.Length;; val it : bool = true
The key here is to understanding a bit more about how operator overloading in F# and .NET works. When you override an operator such as (+) the compiler generates a method named op_Addition. So when a .NET compiler sees “x + y” it looks for a method with that name. Similarly it loops for op_Subtraction, op_BitwiseOr, etc.
Since F# allows you to define symbolic operators (functions with symbols for names) you can overload operators that aren’t valid in C# or VB.NET, for example (==>):
// Rotate the vector about the X axis static member (==>) (vec : Vector, rads : float) = let (Vector(x,y,z)) = vec let x' = x * cos rads - y * sin rads let y' = x * sin rads + y * cos rads let z' = z Vector(x', y', z)
> let v = Vector(10.0, 0.0, 0.0);; val v : Vector = Vector (10.0,0.0,0.0) > v ==> System.Math.PI;; val it : Vector = Vector (-10.0,0.0,0.0)
> v ==> (System.Math.PI / 2.0);; val it : Vector = Vector (0.0,10.0,0.0)
While F# recognizes these symbolic operators, to call them from C# you need to use the ‘long name’ which in the previous example would be op_EqualsEqualsGreater.
To override the equals operator in C# requires that you define (==), which in turn generate a method called op_Equality. However, defining (==) in F# generates a method called op_EqualsEquals.
To properly overload the equals and not equals operators in F# you must use the F# operators for equality (=) and (<>).
// Equals static member (=) (lhs, rhs) = lhs.Length = rhs.Length // Not equals static member (<>) (lhs : Vector, rhs) = not (lhs == rhs)
Improper use of Range Expressions
Consider the following two loops:
for i in [1 .. 10000000] do ... for i in 1 .. 10000000 do ...
They may not look that different, but they have dramatically different performance profiles. You can find these differences detailed in the recently updated F# Language Specification. Both are examples of range expression, which is a way to create a sequence between to indexes. However, the subtlety here is that the first generates a full list, while the second is just the raw sequence.
So while one requires ten million memory allocations to generate the list, the second loop doesn’t. More importantly, the compiler can optimize the second loop into a standard for loop over a single integer. (And not need to do any memory allocation at all!)