Basics 22 - Inline Functions

A Simple Case... Not Really

Let's take a simple example:

let add x y = x + y

let result = add 1 2

So far so good. We created a function named add. Since no type declarations are used, we assume that x, y and return value are all generic types. We can use this function as long as x + y is possible. Now, let's try this:

let add x y = x + y

let result = add 1 2 // No problem
let result = add 1.0 2.0 // Compiler error... expects int
let result = add "1" "2" // Compiler error... expects int

This is unexpected. add 1 2 is working fine, but add 1.0 2.0 and add "1" "2" are not.

Similarly, try this:

let add x y = x + y

let result = add "1" "2" // No problem
let result = add 1.0 2.0 // Compiler error... expects string
let result = add 1 2 // Compiler error... expects string

Roughly the same, but this time add "1" "2" is working fine, and others are not.

Type Inference is not Generics (not always)

It took me a long time to realize the following:

  1. The first come, first served nature of F# compiler

  2. The absence of type declarations doesn't always translate into generics

When the line of code let result = add 1 2 is encountered (where 1 and 2 are int values), type inference assumes add function is int -> int-> int.

In the other example, when let result = add "1" "2" is encountered (where "1" and "2" are string values), type inference assumes add function is string -> string -> string.

To complicate the matter, let's try this now:

let apply f x = f x

apply (printfn "%A") 1 // No problem, 1
apply (printfn "%A") 1.0 // No problem, 1.0
apply (printfn "%A") "1" // No problem, "1"

Everything works fine... duh!

Now what happens when apply (printfn "%A") 1 is encountered? Isn't 1 an int, and shouldn't the parameter x of apply be marked as int?

Generics and the Burden of Generalization

Let's revisit the apply function:

let apply f x = f x

What's the intent here:

  1. Create a function called apply

  2. Takes 2 parameters f and x

  3. Call f with x, and the return value of f is the return value of apply

Pay attention... there is no uncertainty or ambiguity anywhere as long as x can be passed to f. This is what I call the Burdern of Generalization. The caller has to make sure f can be called with x.

Now, let's go back to add function:

let add x y = x + y

What's the intent here:

  1. Create a function called add

  2. Takes 2 parameters x and y

  3. Do x + y, and return the result

Pay attention again... do you notice any uncertainty or ambiguity? There is no way to ensure that x and y can be added. You can call add with any values... int, float, int option and whatnot. If + was a universal operation (+ for any 2 things), there was no problem. + is a context-sensitive operation. + for int, + for string and + for student (user-defined record or discriminated union) are completely different ideas.

When let add x y = x + y is encountered, F# compiler is in wait and watch mode. It's waiting to see what follows... as soon as let result = add 1 2 is encountered, type inference stitches everything together.

This is what I call the case of the Unresolved Burden of Generalization. There are 2 ways this issue can be solved:

  1. Constraints

  2. Inline Functions

Constraints in generics require a dedicated article. So more on constraints later.

Inline Functions

Inline functions are created with let inline:

let inline add x y = x + y

Inline functions are expanded and integrated into the calling code.

let inline add x y = x + y

let result = add 1 2 // let result = 1 + 2
let result = add 1.0 2.0 // let result = 1.0 + 2.0
let result = add "1" "2" // let result = "1" + "2"

There is no actual call to add happening here. Everything is expanded in line. With inline functions, there is no generalization required.

If you have reached so far, congratulations.

Keep reading!