79

The inline keyword in F# seems to me to have a somewhat different purpose than what I'm used to in e.g. C. For example, it seems to affect a function's type (what are "statically resolved type parameters"? Aren't all F# types resolved statically?)

When should I be using inline functions?

J Cooper
  • 16,891
  • 12
  • 65
  • 110

4 Answers4

88

The inline keyword indicates that a function definition should be inserted inline into any code which uses it. Most of the time, this will not have any effect on the type of the function. However, in rare cases, it can lead to a function which has a more general type, since there are constraints which cannot be expressed in the compiled form of the code in .NET, but which can be enforced when the function is being inlined.

The primary case where this applies is the use of operators.

let add a b = a + b

will have a monomorphic inferred type (probably int -> int -> int, but it could be something like float -> float -> float if you have code which uses this function at that type instead). However, by marking this function inline, the F# compiler will infer a polymorphic type:

let inline add a b = a + b
// add has type ^a ->  ^b ->  ^c when ( ^a or  ^b) : (static member ( + ) :  ^a *  ^b ->  ^c)

There is no way to encode this type constraint in a first class way in compiled code on .NET. However, the F# compiler can enforce this constraint at the site where it inlines the function, so that all operator uses are resolved at compile time.

The type parameters ^a, ^b, and ^c are "statically resolved type parameters", which means that the types of the arguments must be statically known at the site where those parameters are being used. This is in contrast to normal type parameters (e.g. 'a, 'b, etc.), where the parameters mean something like "some type which will be supplied later, but which can be anything".

kvb
  • 54,864
  • 2
  • 91
  • 133
  • 3
    I'm sorry but I'm just starting to learn F# and I don't understand your answer at all, can you tell me some URL where the concepts you used are explained? – knocte Jan 15 '14 at 14:04
  • 2
    ``p:'a`` - requires that ``p`` be a descendent of type ``'a`` such as ``int``, ``obj``, ``'a when 'a :> SomeBaseType``, etc. On the other hand, ``p:^a`` - requires that ``p`` be a type that supports some features or subtype at the invocation points in the code by doing so directly or by having opened a supporting module or accessing an extension methods class, etc. – George Aug 16 '14 at 16:53
  • knocte >> https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/generics/statically-resolved-type-parameters – Peheje Mar 27 '21 at 11:25
53

You should use inline when you need to define a function that must have its type (re)evaluated at the site of each usage, as opposed to a normal function, which will have its type evaluated (inferred) only at the site of first usage, and then be regarded as being statically typed with that first inferred type signature everywhere else thereafter.

In the inline case, the function definition is effectively generic/ polymorphic, whereas in the normal (none-inline) case, the function is statically (and often implicitly) typed.

So, if you use inline, the following code:

let inline add a b = a + b

[<EntryPoint>]
let main args = 

    let one = 1
    let two = 2
    let three = add one two
    // here add has been compiled to take 2 ints and return an int

    let dog = "dog"
    let cat = "cat"
    let dogcat = add dog cat
    // here add has been compiled to take 2 strings and return a string

    printfn "%i" three
    printfn "%s" dogcat   

    0

will compile, build and run to produce the following output:

3  
dogcat

In other words, the same add function definition has been used to produce both a function that adds two integers, and a function that concatenates two strings (in fact the underlying operator overloading on + is also achieved under the hood using inline).

Whereas this code, identical except that the add function is no longer declared inline:

let add a b = a + b

[<EntryPoint>]
let main args = 

    let one = 1
    let two = 2
    let three = add one two
    // here add has been compiled to take 2 ints and return an int

    let dog = "dog"
    let cat = "cat"
    let dogcat = add dog cat
    // since add was not declared inline, it cannot be recompiled
    // and so we now have a type mismatch here

    printfn "%i" three
    printfn "%s" dogcat   

    0

will NOT compile, failing with this complaint:

    let dogcat = add dog cat
                     ^^^ - This expression was expected to have type int
                           but instead has type string

A good example of where using inline is appropriate, is when you want to define a high order function (HOF, i.e. a function taking (other) functions as arguments), e.g. a generic function to reverse the order of the application of arguments of a function with 2 arguments, e.g.

let inline flip f x y = f y x

as is done in the answer from @pad to this question Different argument order for getting N-th element of Array, List or Seq.

Nick
  • 6,366
  • 5
  • 43
  • 62
david.barkhuizen
  • 5,239
  • 4
  • 36
  • 38
36

When should I be using inline functions?

The most valuable application of the inline keyword in practice is inlining higher-order functions to the call site where their function arguments are also inlined in order to produce a singly fully-optimized piece of code.

For example, the inline in the following fold function makes it 5× faster:

  let inline fold f a (xs: _ []) =
     let mutable a = a
     for i=0 to xs.Length-1 do
        a <- f a xs.[i]
     a

Note that this bears little resemblance to what inline does in most other languages. You can achieve a similar effect using template metaprogramming in C++ but F# can also inline between compiled assemblies because inline is conveyed via .NET metadata.

J D
  • 48,105
  • 13
  • 171
  • 274
  • 2
    Is the standard-library fold inlined? – J Cooper Sep 21 '10 at 17:19
  • 5
    @J Cooper: No but it does convert its function argument to an optimized closure which helps for certain types. The `fold` I gave is ~3× faster than the built-in `Array.fold` when applied to complex numbers, for example. – J D Sep 21 '10 at 19:47
  • Interesting. Would there be a downside to the built-in using `inline`? – J Cooper Sep 21 '10 at 20:47
  • 3
    Man, 3-5x faster... that's a lot! Do you ever resort to using this inline fold over the standard-library folds when developing performance critical libraries? – Stephen Swensen Sep 21 '10 at 22:51
  • 1
    @Stephen: Yes, all the time. We use `inline` a lot and (counterintuitively) not because of the inlining it performs! – J D Sep 22 '10 at 13:09
  • @JonHarrop Yeah, `inline` can also give much more useful function types as noted in kvb's answer. This is really amazing. – Display Name Sep 03 '13 at 18:25
11

The F# component design guidelines only mention a little about this. My recommendation (that aligns well with what's said there) is:

  • Don't use inline
    • Exception: you might consider using inline when writing mathematical libraries to be consumed by other F# code and you want to write functions that are generic over different numeric data types.

There are lots of other "interesting" uses of inline and static member constraints for "duck-typing" kinds of scenarios that work a bit like C++ templates. My advice is to avoid all of that like the plague.

@kvb's answer goes into more depth about what 'static type constraints' are.

Brian
  • 117,631
  • 17
  • 236
  • 300
  • 8
    The `inline` keyword is also extremely useful beyond mathematical libraries. For example, in the context of data structures where it can be used to compose concrete data structures from abstract ones without incurring any run-time performance penalty. – J D Sep 21 '10 at 08:38
  • 1
    How am I supposed to implement type-classes without inline ? – Luiz Felipe Feb 11 '17 at 16:47
  • This advice is probably outdated. inline works well, SRTP were just improved to be more usable. They certainly are more advanced, but I think it's no longer a good idea to avoid them. – VoronoiPotato Dec 06 '22 at 21:23