This blog post explains five F#-specific features who simplify and stabilize the implementation of vector algebra:
We will use these features to define an infix dot product operator who is compile-time type-safe and generic.
Dot Product Operator Implementation
let inline (.*) xs ys = Seq.map2 (*) xs ys |> Seq.sum
The above line of code does not contain any type annotations. However, thanks to the F# compiler's built-in Hindley-Milner type inference algorithm with automatic generalization, the code is compiled with the following strongly typed signature:
val inline (.*) :
xs:seq< ^a> -> ys:seq< ^b> -> ^c
when (^a or ^b) : (static member (*) : ^a * ^b -> ^c) and
^c : (static member (+) : ^c * ^c -> ^c) and
^c : (static member get_zero : unit -> ^c)
The inline keyword lets the compiler inline the operation's implementation body at the call site, where the types of arguments are well-known. As a consequence, the types of the operation's parameters and result can be resolved statically (i.e., at compile time) according to the contexts of different kinds of call sites. You can recognize such types by the prefix ^ in the statically resolved type parameters (also called head-type parameters) of the inferred signature. Any combination of arguments can be passed to the operation, as long as they satisfy the following constraints:
- The first (left-hand side) argument must be a sequence of ^as.
- The second (right-hand side) argument must be a sequence of ^bs.
- Either the type of ^a, or the type of ^b, must implement a * operator who returns an instance of ^c.
- The type of ^c must implement a + operator and must have a zero representation.
Any numeric primitive type satisfies the constraints for ^a, ^b, and ^c. You can also define your own compatible F# types (records, unions, classes, structs, or interfaces). Another alternative is to use built-in numeric types together with units of measure, which we are going to demonstrate next.
Example 1: Calculate an area
This example uses the m (metre) unit of measure predefined in the FSharp.Core library.
let lengths = [3<m>; 5<m>] // a list of integer metre values
let widths = [4<m>; 2<m>] // another list of integer metre values
let area = lengths .* widths // 22 square metres, typed as 22<m ^ 2>
Example 2: Calculate Speed
This example uses the m (metre) and s (second) units of measure predefined in the FSharp.Core library.
let durations = [10.3<s>; 4.25<s>] // a list of float second values
let accelerations = [3.1<m/s^2>; 4.2<m/s^2>] // a list of float "m per square second" values
let speed = durations .* accelerations // 49.78 metres per second, typed as 49.78<m/s>
Example 3: Calculate Portfolio Value
This example uses custom units of measure.
[<Measure>] type CHF
[<Measure>] type EUR
let quantities = [17M; 12M; 14M] // a list of decimal values
let pricesEUR = [3.75M<EUR>; 4.8M<EUR>; 2.5M<EUR>] // a list of decimal Euro values
let exRate = 1.0542M<CHF/EUR> // a decimal "Swiss Franc per Euro" exchange rate
let totalEUR = quantities .* pricesEUR // 156.35 Euro, typed as 156.35M<EUR>
let totalCHF = totalEUR * exRate // 164.82417 Swiss Francs, typed as 164.824170M<CHF>
Writing just a single line of code, we have defined an operation, with compile-time type safety, for calculating dot products in different domains such as areas, speeds, or portfolio values. The DRY principle (don't repeat yourself) is usually associated with mainstream OO-first languages such as C# or Java. However, the functional-first language F# lets you apply the principle more consequently, without sacrificing OO.