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
- The second (right-hand side) argument must be a sequence of
- Either the type of
^a, or the type of
^b, must implement a
*operator who returns an instance of
- The type of
^cmust implement a
+operator and must have a zero representation.
Any numeric primitive type satisfies the constraints for
^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
open Microsoft.FSharp.Data.UnitSystems.SI.UnitSymbols 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
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.