Events
Mar 17, 11 PM - Mar 21, 11 PM
Join the meetup series to build scalable AI solutions based on real-world use cases with fellow developers and experts.
Register nowThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
F# has two major feature areas that deal in the space of low-level programming:
byref
/inref
/outref
types, which are managed pointers. They have restrictions on usage so that you cannot compile a program that is invalid at run time.byref
-like struct, which is a struct 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
There are three forms of byref
:
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.A 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: %O{dt}"
let usage =
let dt = DateTime.Now
f &dt // Pass a pointer to 'dt'
To write to the pointer by using an outref<'T>
or byref<'T>
, you must also make the value you grab a pointer to mutable
.
open System
let f (dt: byref<DateTime>) =
printfn $"Now: %O{dt}"
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 byref<'T>
.
Consider the following code:
let f (x: inref<SomeStruct>) = x.SomeField
Semantically, this means the following:
x
pointer may only use it to read the value.struct
fields nested within SomeStruct
are given type inref<_>
.The following is also true:
x
.SomeStruct
is immutable by virtue of x
being an inref
.However, for F# value types that are immutable, the this
pointer is inferred to be an inref
.
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 written to. Unexpectedly, outref<'T>
permits reading the underlying value despite its name. This is for compatibility purposes.
Semantically, outref<'T>
is no different than byref<'T>
, except for one difference: methods with outref<'T>
parameters are implicitly constructed into a tuple return type, just like when calling a method with an [<Out>]
parameter.
type C =
static member M1(x, y: _ outref) =
y <- x
true
match C.M1 1 with
| true, 1 -> printfn "Expected" // Fine with outref, error with byref
| _ -> printfn "Never matched"
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 |
---|---|
ref return value |
outref<'T> |
ref readonly return value |
inref<'T> |
in ref parameter |
inref<'T> |
out ref parameter |
outref<'T> |
The following table shows what F# emits:
F# construct | Emitted construct |
---|---|
inref<'T> argument |
[In] attribute on argument |
inref<'T> return |
modreq attribute on value |
inref<'T> in abstract slot or implementation |
modreq on argument or return |
outref<'T> argument |
[Out] attribute on argument |
An inref<'T>
type is inferred by the F# compiler in the following cases:
IsReadOnly
attribute.this
pointer on a struct type that has no mutable fields.inref<_>
pointer.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 inref<System.DateTime>
.
In addition to the byref
/inref
/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.
A "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:
async
methods or lambda expressions).Action<>
, Func<>
), interfaces (IEnumerable<>
, IComparable<>
) and generic arguments with a user-provided accumulator function (String.string Create<TState>(int length, TState state, SpanAction<char, TState> action)
).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 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 squareAndPrint (data : byref<int>) =
let squared = data*data // data is implicitly dereferenced
printfn $"%d{squared}"
To return a value byref, the variable that contains the value must live longer than the current scope.
Also, to return byref, use &value
(where value is a variable that lives longer than the current scope).
let mutable sum = 0
let safeSum (bytes: Span<byte>) =
for i in 0 .. bytes.Length - 1 do
sum <- sum + int bytes[i]
&sum // sum lives longer than the scope of this function.
To avoid the implicit dereference, such as passing a reference through multiple chained calls, use &x
(where 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[0]
[<EntryPoint>]
let main argv =
let c = C()
printfn $"Original sequence: %O{c}"
let v = &c.FindLargestSmallerThan 16
v <- v*2 // Directly assign to the byref return
printfn $"New sequence: %O{c}"
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
A 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 or not.
.NET feedback
.NET is an open source project. Select a link to provide feedback:
Events
Mar 17, 11 PM - Mar 21, 11 PM
Join the meetup series to build scalable AI solutions based on real-world use cases with fellow developers and experts.
Register now