F# tips weekly #13: Operators

F# tips weekly #13: Operators

Using operators like =, +, && or |> is daily bread of writing F#. Let's look at how they work under the hood.

Operator is a function

Every operator can be used as standard function by enclosing it in parentheses, for example

(+) 1 2

is the same as

1 + 2

This can be used with currying in place of lambda function. For example, we can write product function like this:

let product xs = xs |> Seq.reduce ((*))

Operator methods

During compilation, operator is replaced with their name - (+) is changed to op_Addition. In fact, using named form of operator is valid F# code:

op_Addition 1 2

Full list of operator names can be found in F# specification on page 34.

Unary operators

Alongside the classic binary operators, that works on two arguments, there are also unary operators. These operators are prefix (are used before its argument). One example is ~~~, operator for bitwise negation.

The most common usage of unary operator is unary variant of -. When - is used before number without space, it is considered unary operator and gets translated into (~-), so compiler can distinguish between unary and binary variant.

That's the reason why missed space after - leads to compiler error:

1 -2

- there is applied as unary operator to 2.

This behavior is specific only to few specific operators, namely +, -, %, %%, &, &&.

Custom operators

We can define new operators using let (op) = ... syntax. For example

let (=~) x y = abs (x - y) < 1e-6

defines =~ operator for comparing floats with ignoring rounding errors.

Note that this operator is defined on floats only. Defining generic operators that work on multiple types is possible, but it is complicated and discouraged, because it can lead to puzzling compiler errors.

New operators are also translated into named form, new name is based on character used, so in our case we get op_EqualsTwiddle.

Redefine operators

We can redefine existing operator by defining a new operator. For example:

let (+) a b = (a + b) % 7

Note that this way we redefined + to work only on ints, + on floats ceases to work.

Operators as static members

Another way is to (re)define operator on custom type as static member:

type Vector2D =
    {
        X: float
        Y: float
    }

    static member (+) (a: Vector2D, b: Vector2D) = { X = a.X + b.X; Y = a.Y + b.Y }

In this case it is quite easy to write generic operator:

type Vector2D =
    {
        X: float
        Y: float
    }

    static member (+) (a: Vector2D, b: Vector2D) = { X = a.X + b.X; Y = a.Y + b.Y }
    static member (*) (a: Vector2D, b: Vector2D) = { X = a.X * b.X; Y = a.Y * b.Y }
    static member (*) (a: Vector2D, b: float) = { X = a.X * b; Y = a.Y * b }

static member operator methods actually works as extension of operators.