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:
The first come, first served nature of F# compiler
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:
Create a function called
apply
Takes 2 parameters
f
andx
Call
f
withx
, and the return value off
is the return value ofapply
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:
Create a function called
add
Takes 2 parameters
x
andy
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:
Constraints
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!