Byrefs

F# tiene dos áreas de características principales que se ocupan del espacio de programación de bajo nivel:

  • Los tipos byref/inref/outref, que son punteros administrados. Tienen restricciones de uso para que no pueda compilar un programa que no sea válido en tiempo de ejecución.
  • Estructura similar a byref, que es una estructura que tiene una semántica similar y las mismas restricciones en tiempo de compilación que byref<'T>. Un ejemplo es Span<T>.

Sintaxis

// 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 y outref

Hay tres formas de byref:

  • inref<'T>, un puntero administrado para leer el valor subyacente.
  • outref<'T>, un puntero administrado para escribir en el valor subyacente.
  • byref<'T>, un puntero administrado para leer y escribir el valor subyacente.

Se puede pasar un byref<'T> donde se espera inref<'T>. Del mismo modo, se puede pasar un objeto byref<'T> donde se espera outref<'T>.

Uso de byrefs

Para usar un inref<'T>, debes obtener un valor de puntero con &:

open System

let f (dt: inref<DateTime>) =
    printfn $"Now: %O{dt}"

let usage =
    let dt = DateTime.Now
    f &dt // Pass a pointer to 'dt'

Para escribir en el puntero mediante outref<'T> o byref<'T>, también debe hacer que el valor que tome un puntero a 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

Si solo estás escribiendo el puntero en lugar de leerlo, considera la posibilidad de usar outref<'T> en lugar de byref<'T>.

Semántica de inref

Observe el código siguiente:

let f (x: inref<SomeStruct>) = x.SomeField

Semánticamente, esto significa lo siguiente:

  • El titular del puntero x solo puede usarlo para leer el valor.
  • Cualquier puntero adquirido en struct campos anidados dentro SomeStruct de se asigna al tipo inref<_>.

También se cumple lo siguiente:

  • No hay ninguna implicación que otros subprocesos o alias no tengan acceso de escritura a x.
  • No hay ninguna implicación que SomeStruct sea inmutable en virtud de x ser inref.

Sin embargo, para los tipos de valor de F# que son inmutables, el this puntero se deduce que es inref.

Todas estas reglas juntas significan que el titular de un inref puntero no puede modificar el contenido inmediato de la memoria a la que se apunta.

Semántica de outref

El propósito de outref<'T> es indicar que el puntero solo se debe escribir en. Inesperadamente, outref<'T> permite leer el valor subyacente a pesar de su nombre. Es para fines de compatibilidad.

Semánticamente, outref<'T> no es diferente de byref<'T>, excepto por una diferencia: los métodos con outref<'T> parámetros se construyen implícitamente en un tipo de valor devuelto de tupla, al igual que al llamar a un método con un [<Out>] parámetro.

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"

Interoperabilidad con C#

C# admite las palabras clave in ref y out ref, además de ref las devoluciones. En la tabla siguiente se muestra cómo interpreta F# lo que emite C#:

Construcción C# Inferencias de F#
ref valor devuelto outref<'T>
ref readonly valor devuelto inref<'T>
Parámetro in ref inref<'T>
Parámetro out ref outref<'T>

En la tabla siguiente se muestra lo que emite F#:

Construcción F# Construcción emitida
Argumento inref<'T> [In] atributo en el argumento
inref<'T> retorno modreq atributo en el valor
inref<'T> en ranura abstracta o implementación modreq en argumento o retorno
Argumento outref<'T> [Out] atributo en el argumento

Reglas de inferencia y sobrecarga de tipos

El compilador de F# deduce un inref<'T> tipo en los casos siguientes:

  1. Parámetro de .NET o tipo de valor devuelto que tiene un atributo IsReadOnly.
  2. Puntero this en un tipo de estructura que no tiene campos mutables.
  3. Dirección de una ubicación de memoria derivada de otro inref<_> puntero.

Cuando se toma una dirección implícita de inref, se prefiere una sobrecarga con un argumento de tipo SomeType a una sobrecarga con un argumento de tipo inref<SomeType>. Por ejemplo:

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)

En ambos casos, las sobrecargas que toman System.DateTime se resuelven en lugar de las sobrecargas que toman inref<System.DateTime>.

Estructuras similares a byref

Además del trío byref/inref/outref, puede definir sus propias estructuras que pueden adherirse a byrefla semántica similar. Esto se realiza a través del atributo IsByRefLikeAttribute:

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 no implica Struct. Ambos deben estar presentes en el tipo.

Una estructura «similar abyref» en F# es un tipo de valor enlazado a la pila. Nunca se asigna en el montón administrado. Una estructura similiar a byref es útil para la programación de alto rendimiento, ya que se aplica con un conjunto de comprobaciones seguras sobre la duración y la no captura. Las reglas son:

  • Se pueden usar como parámetros de función, parámetros de método, variables locales, devoluciones de método.
  • No pueden ser miembros estáticos o de instancia de una clase o una estructura normal.
  • No se pueden capturar mediante ninguna construcción de cierre (métodos async o expresiones lambda).
  • No se pueden usar como parámetro genérico.

Este último punto es fundamental para la programación de estilo de canalización de F#, ya que |> es una función genérica que parametriza sus tipos de entrada. Esta restricción puede ser relajada en |> el futuro, ya que está insertada y no realiza ninguna llamada a funciones genéricas no insertadas en su cuerpo.

Aunque estas reglas restringen fuertemente el uso, lo hacen para cumplir la promesa de informática de alto rendimiento de una manera segura.

Byref devuelve

Byref devuelve funciones o miembros de F# que se pueden producir y consumir. Al consumir un método que devuelve byref, el valor se desreferencia implícitamente. Por ejemplo:

let squareAndPrint (data : byref<int>) =
    let squared = data*data    // data is implicitly dereferenced
    printfn $"%d{squared}"

Para devolver un valor byref, la variable que contiene el valor debe residir más tiempo que el ámbito actual. Además, para devolver byref, usa &value (donde value es una variable que reside más tiempo que el ámbito actual).

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.

Para evitar la desreferenciación implícita, como pasar una referencia a través de varias llamadas encadenadas, usa &x (donde x es el valor).

También puedes asignar directamente a un valor devuelto byref. Ten en cuenta el siguiente programa (altamente imperativo):

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

Éste es el resultado:

Original sequence: 1 3 7 15 31 63 127 255 511 1023
New sequence:      1 3 7 30 31 63 127 255 511 1023

Ámbito de byrefs

Un valor enlazado a let no puede hacer que su referencia supere el ámbito en el que se definió. Por ejemplo, la siguiente vista no se admite:

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!
    ()

Esto le impide obtener resultados diferentes dependiendo de si se compila con optimizaciones o no.