Skip to content
AgileLogics
6 min read

Inline Functions and SRTP

Prerequisites: Generics

.NET generics can't say "any type that has a + operator" or "any type with a Length property". F# can, with statically resolved type parameters (SRTP), written ^T and available in inline functions. The compiler copies the function body into each call site and checks the requirement for the concrete type there.

Generic math

let inline sumOfSquares xs =
    xs |> List.sumBy (fun x -> x * x)

printfn "%d" (sumOfSquares [1; 2; 3])
printfn "%.2f" (sumOfSquares [0.5; 1.5])
printfn "%M" (sumOfSquares [1.1m; 2.0m])

let inline average xs =
    let total = List.sum xs
    LanguagePrimitives.DivideByInt total (List.length xs)

printfn "%.2f" (average [1.0; 2.0; 4.0])
printfn "%M" (average [10m; 20m])
14
2.50
5.21
2.33
15

LanguagePrimitives.DivideByInt divides any float-like type by an int, and LanguagePrimitives.GenericZero and GenericOne give 0 and 1 of "whatever numeric type is being used here".

Member constraints

An SRTP constraint can require any member:

let inline lengthOf (x: ^T) = (^T: (member Length: int) x)

printfn "%d" (lengthOf "hello")
printfn "%d" (lengthOf [| 1; 2; 3 |])
printfn "%d" (lengthOf [ 'a'; 'b' ])

type Box = { Label: string } with
    member this.Length = this.Label.Length * 100

printfn "%d" (lengthOf { Label = "big" })
5
3
2
300

Strings, arrays, lists, and your own record all have a Length property — they share no interface, but lengthOf accepts all of them. Passing something without Length is a compile error.

Operators on your types

Built-in generic functions use SRTP too, so if your type defines the right static members, it works with them:

type Money =
    { Cents: int64 }
    static member (+) (a: Money, b: Money) = { Cents = a.Cents + b.Cents }
    static member Zero = { Cents = 0L }
    override this.ToString() = sprintf "$%d.%02d" (this.Cents / 100L) (this.Cents % 100L)

let items = [ { Cents = 1999L }; { Cents = 501L }; { Cents = 250L } ]
printfn "%O" (List.sum items)
$27.50

List.sum requires + and a Zero member — Money has both.

Use sparingly

  • inline makes every call site bigger and error messages harder to read. Use it for small, genuinely generic numeric or utility functions.
  • For most "works with several types" needs, interfaces, plain generics, or passing a function parameter are simpler.
  • inline also helps performance when a small function takes a lambda (the lambda can be inlined too), which is why many Array functions are inline.