F# has two major feature areas that deal in the space of low-level programming:
outreftypes, which are a managed pointers. They have restrictions on usage so that you cannot compile a program that is invalid at runtime.
byref-like struct, which is a structure that has similar semantics and the same compile-time restrictions as
byref<'T>. One example is Span<T>.
// Byref types as parameters let f (x: byref<'T>) = () let g (x: inref<'T>) = () let h (x: outref<'T>) = () // Calling a function with a byref parameter let mutable x = 3 f &x // Declaring a byref-like struct open System.Runtime.CompilerServices [<Struct; IsByRefLike>] type S(count1: int, count2: int) = member x.Count1 = count1 member x.Count2 = count2
Byref, inref, and outref
There are three forms of
inref<'T>, a managed pointer for reading the underlying value.
outref<'T>, a managed pointer for writing to the underlying value.
byref<'T>, a managed pointer for reading and writing the underlying value.
byref<'T> can be passed where an
inref<'T> is expected. Similarly, a
byref<'T> can be passed where an
outref<'T> is expected.
To use a
inref<'T>, you need to get a pointer value with
open System let f (dt: inref<DateTime>) = printfn "Now: %s" (dt.ToString()) let usage = let dt = DateTime.Now f &dt // Pass a pointer to 'dt'
To write to the pointer by using an
byref<'T>, you must also make the value you grab a pointer to
open System let f (dt: byref<DateTime>) = printfn "Now: %s" (dt.ToString()) dt <- DateTime.Now // Make 'dt' mutable let mutable dt = DateTime.Now // Now you can pass the pointer to 'dt' f &dt
If you are only writing the pointer instead of reading it, consider using
outref<'T> instead of
Consider the following code:
let f (x: inref<SomeStruct>) = x.SomeField
Semantically, this means the following:
- The holder of the
xpointer may only use it to read the value.
- Any pointer acquired to
structfields nested within
SomeStructare given type
The following is also true:
- There is no implication that other threads or aliases do not have write access to
- There is no implication that
SomeStructis immutable by virtue of
However, for F# value types that are immutable, the
this pointer is inferred to be an
All of these rules together mean that the holder of an
inref pointer may not modify the immediate contents of the memory being pointed to.
The purpose of
outref<'T> is to indicate that the pointer should only be read from. Unexpectedly,
outref<'T> permits reading the underlying value despite its name. This is for compatibility purposes. Semantically,
outref<'T> is no different than
Interop with C#
C# supports the
in ref and
out ref keywords, in addition to
ref returns. The following table shows how F# interprets what C# emits:
|C# construct||F# infers|
The following table shows what F# emits:
|F# construct||Emitted construct|
Type inference and overloading rules
inref<'T> type is inferred by the F# compiler in the following cases:
- A .NET parameter or return type that has an
thispointer on a struct type that has no mutable fields.
- The address of a memory location derived from another
When an implicit address of an
inref is being taken, an overload with an argument of type
SomeType is preferred to an overload with an argument of type
inref<SomeType>. For example:
type C() = static member M(x: System.DateTime) = x.AddDays(1.0) static member M(x: inref<System.DateTime>) = x.AddDays(2.0) static member M2(x: System.DateTime, y: int) = x.AddDays(1.0) static member M2(x: inref<System.DateTime>, y: int) = x.AddDays(2.0) let res = System.DateTime.Now let v = C.M(res) let v2 = C.M2(res, 4)
In both cases, the overloads taking
System.DateTime are resolved rather than the overloads taking
In addition to the
outref trio, you can define your own structs that can adhere to
byref-like semantics. This is done with the IsByRefLikeAttribute attribute:
open System open System.Runtime.CompilerServices [<IsByRefLike; Struct>] type S(count1: Span<int>, count2: Span<int>) = member x.Count1 = count1 member x.Count2 = count2
IsByRefLike does not imply
Struct. Both must be present on the type.
byref-like" struct in F# is a stack-bound value type. It is never allocated on the managed heap. A
byref-like struct is useful for high-performance programming, as it is enforced with set of strong checks about lifetime and non-capture. The rules are:
- They can be used as function parameters, method parameters, local variables, method returns.
- They cannot be static or instance members of a class or normal struct.
- They cannot be captured by any closure construct (
asyncmethods or lambda expressions).
- They cannot be used as a generic parameter.
This last point is crucial for F# pipeline-style programming, as
|> is a generic function that parameterizes its input types. This restriction may be relaxed for
|> in the future, as it is inline and does not make any calls to non-inlined generic functions in its body.
Although these rules very strongly restrict usage, they do so to fulfill the promise of high-performance computing in a safe manner.
Byref returns from F# functions or members can be produced and consumed. When consuming a
byref-returning method, the value is implicitly dereferenced. For example:
let safeSum(bytes: Span<byte>) = let mutable sum = 0 for i in 0 .. bytes.Length - 1 do sum <- sum + int bytes.[i] sum let sum = safeSum(mySpanOfBytes) printfn "%d" sum // 'sum' is of type 'int'
To avoid the implicit dereference, such as passing a reference through multiple chained calls, use
x is the value).
You can also directly assign to a return
byref. Consider the following (highly imperative) program:
type C() = let mutable nums = [| 1; 3; 7; 15; 31; 63; 127; 255; 511; 1023 |] override __.ToString() = String.Join(' ', nums) member __.FindLargestSmallerThan(target: int) = let mutable ctr = nums.Length - 1 while ctr > 0 && nums.[ctr] >= target do ctr <- ctr - 1 if ctr > 0 then &nums.[ctr] else &nums. [<EntryPoint>] let main argv = let c = C() printfn "Original sequence: %s" (c.ToString()) let v = &c.FindLargestSmallerThan 16 v <- v*2 // Directly assign to the byref return printfn "New sequence: %s" (c.ToString()) 0 // return an integer exit code
This is the output:
Original sequence: 1 3 7 15 31 63 127 255 511 1023 New sequence: 1 3 7 30 31 63 127 255 511 1023
Scoping for byrefs
let-bound value cannot have its reference exceed the scope in which it was defined. For example, the following is disallowed:
let test2 () = let x = 12 &x // Error: 'x' exceeds its defined scope! let test () = let x = let y = 1 &y // Error: `y` exceeds its defined scope! ()
This prevents you from getting different results depending on if you compile with optimizations on or off.
Send feedback about: