Basics 22 - Inline Functions
Love and hate relationship with programming. Always learning programming and Jazz.
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
applyTakes 2 parameters
fandxCall
fwithx, and the return value offis 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
addTakes 2 parameters
xandyDo
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!