Novedades de F# 6

F# 6 agrega varias mejoras al lenguaje F# y a F# interactivo. Se publica con .NET 6.

Puede descargar el SDK de .NET más reciente de la página de descargas de .NET.

Introducción

F# 6 está disponible en todas las distribuciones de .NET Core y las herramientas de Visual Studio. Para más información, vea Introducción a F#.

task {…}

F# 6 incluye compatibilidad nativa con la creación de tareas de .NET en código de F#. Por ejemplo, considere el siguiente código de F# para crear una tarea compatible con .NET:

let readFilesTask (path1, path2) =
   async {
        let! bytes1 = File.ReadAllBytesAsync(path1) |> Async.AwaitTask
        let! bytes2 = File.ReadAllBytesAsync(path2) |> Async.AwaitTask
        return Array.append bytes1 bytes2
   } |> Async.StartAsTask

Con F# 6, este código se puede volver a escribir de la siguiente manera.

let readFilesTask (path1, path2) =
   task {
        let! bytes1 = File.ReadAllBytesAsync(path1)
        let! bytes2 = File.ReadAllBytesAsync(path2)
        return Array.append bytes1 bytes2
   }

La compatibilidad con tareas estaba disponible para F# 5 a través de las excelentes bibliotecas TaskBuilder.fs y Ply. Debe ser sencillo migrar código a la compatibilidad integrada. Sin embargo, hay algunas diferencias: los espacios de nombres y la inferencia de tipos difieren ligeramente entre la compatibilidad integrada y estas bibliotecas, y es posible que se necesiten algunas anotaciones de tipo adicionales. Si es necesario, puede seguir usando estas bibliotecas de la comunidad con F# 6 si hace referencia a ellas explícitamente y abre los espacios de nombres correctos en cada archivo.

Utilizar task {…} es muy similar a usar async {…}. Usar task {…} tiene varias ventajas sobre async {…}:

  • La sobrecarga de task {...} es menor, lo que posiblemente mejora el rendimiento en las rutas de acceso al código activas en las que el trabajo asincrónico se ejecuta rápidamente.
  • La depuración de los seguimientos de pila y paso a paso para task {…} es mejor.
  • La interoperación con paquetes .NET que esperan o generan tareas es más fácil.

Si está familiarizado con async {…}, hay algunas diferencias que debe tener en cuenta:

  • task {…} ejecuta inmediatamente la tarea en el primer punto await.
  • task {…} no propaga implícitamente un token de cancelación.
  • task {…} no realiza comprobaciones de cancelación implícitas.
  • task {…} no admite llamadas de cola asincrónicas. Esto significa que el uso de return! .. de forma recursiva puede dar lugar a desbordamientos de pila si no hay esperas asincrónicas intermedias.

En general, debe considerar la posibilidad de usar task {…} antes que async {…} en el código nuevo si está interoperando con bibliotecas de .NET que usan tareas y, si no se basa en llamadas de cola de código asincrónicas o en la propagación implícita de tokens de cancelación. En el código existente, solo debe cambiar a task {…} una vez que haya revisado el código para asegurarse de que no se basa en las características mencionadas anteriormente de async {…}.

Esta característica implementa F# RFC FS-1097.

Sintaxis de indexación más sencilla con expr[idx]

F# 6 permite la sintaxis expr[idx] para las colecciones de indexación y segmentación.

Hasta F# 5 inclusive, F# ha usado expr.[idx] como sintaxis de indexación. Permitir el uso de expr[idx] se basa en los comentarios repetidos de las personas que aprenden F# o ven F# por primera vez que indican que el uso de la indexación de notación de puntos es una divergencia innecesaria de la práctica estándar del sector.

Este no es un cambio importante porque, de forma predeterminada, no se emiten advertencias sobre el uso de expr.[idx]. Sin embargo, se emiten algunos mensajes informativos que sugieren aclaraciones del código. También puede activar más mensajes informativos. Por ejemplo, puede activar una advertencia informativa opcional (/warnon:3566) para iniciar el informe de los usos de la notación expr.[idx]. Para más información, vea Notación de indexador.

En el nuevo código, se recomienda el uso sistemático de expr[idx] como sintaxis de indexación.

Esta característica implementa F# RFC FS-1110.

Representaciones de estructura de patrones activos parciales

F# 6 amplía la característica "patrones activos" con representaciones de estructura opcionales para patrones activos parciales. Esto le permite usar un atributo para restringir un patrón activo parcial para devolver una opción de valor:

[<return: Struct>]
let (|Int|_|) str =
   match System.Int32.TryParse(str) with
   | true, int -> ValueSome(int)
   | _ -> ValueNone

Se requiere el uso del atributo. En los sitios de utilización, el código no cambia. El resultado neto es que se reducen las asignaciones.

Esta característica implementa F# RFC FS-1039.

Operaciones personalizadas sobrecargadas en expresiones de cálculo

F# 6 permite usar CustomOperationAttribute en los métodos sobrecargados.

Tenga en cuenta el uso siguiente de un generador de expresiones de cálculo content:

let mem = new System.IO.MemoryStream("Stream"B)
let content = ContentBuilder()
let ceResult =
    content {
        body "Name"
        body (ArraySegment<_>("Email"B, 0, 5))
        body "Password"B 2 4
        body "BYTES"B
        body mem
        body "Description" "of" "content"
    }

Aquí, la operación personalizada body toma un número variable de argumentos de diferentes tipos. Esto es compatible con la implementación del generador siguiente, que usa la sobrecarga:

type Content = ArraySegment<byte> list

type ContentBuilder() =
    member _.Run(c: Content) =
        let crlf = "\r\n"B
        [|for part in List.rev c do
            yield! part.Array[part.Offset..(part.Count+part.Offset-1)]
            yield! crlf |]

    member _.Yield(_) = []

    [<CustomOperation("body")>]
    member _.Body(c: Content, segment: ArraySegment<byte>) =
        segment::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, bytes: byte[]) =
        ArraySegment<byte>(bytes, 0, bytes.Length)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, bytes: byte[], offset, count) =
        ArraySegment<byte>(bytes, offset, count)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, content: System.IO.Stream) =
        let mem = new System.IO.MemoryStream()
        content.CopyTo(mem)
        let bytes = mem.ToArray()
        ArraySegment<byte>(bytes, 0, bytes.Length)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, [<ParamArray>] contents: string[]) =
        List.rev [for c in contents -> let b = Text.Encoding.ASCII.GetBytes c in ArraySegment<_>(b,0,b.Length)] @ c

Esta característica implementa F# RFC FS-1056.

Patrones "as"

En F# 6, el lado derecho de un patrón as ahora puede ser un patrón. Es importante cuando una prueba de tipo ha dado un tipo más seguro a una entrada. Por ejemplo, considere el siguiente código:

type Pair = Pair of int * int

let analyzeObject (input: obj) =
    match input with
    | :? (int * int) as (x, y) -> printfn $"A tuple: {x}, {y}"
    | :? Pair as Pair (x, y) -> printfn $"A DU: {x}, {y}"
    | _ -> printfn "Nope"

let input = box (1, 2)

En cada caso de patrón, se prueban los tipos del objeto de entrada. El lado derecho del patrón as puede ser ahora un patrón adicional, que puede coincidir con el objeto en el tipo más seguro.

Esta característica implementa F# RFC FS-1105.

Revisiones de sintaxis de sangría

F# 6 quita una serie de incoherencias y limitaciones en su uso de la sintaxis compatible con la sangría. Consulte RFC FS-1108. Esto resuelve 10 problemas significativos resaltados por los usuarios de F# desde F# 4.0.

Por ejemplo, en F# 5 se permitía el código siguiente:

let c = (
    printfn "aaaa"
    printfn "bbbb"
)

Sin embargo, no se permitía el código siguiente (generaba una advertencia):

let c = [
    1
    2
]

En F# 6, se permiten ambos. Esto hace que F# sea más sencillo y más fácil de aprender. El colaborador de la comunidad de F# Hadrian Tang ha liderado el camino, incluidas las pruebas sistemáticas notables y muy valiosas de la característica.

Esta característica implementa F# RFC FS-1108.

Conversiones implícitas adicionales

En F# 6, hemos activado la compatibilidad con conversiones adicionales "implícitas" y "dirigidas por tipos", como se describe en RFC FS-1093.

Este cambio aporta tres ventajas:

  1. Se requieren menos conversiones a tipo básico explícitas.
  2. Se requieren menos conversiones de entero explícitas.
  3. Se agrega compatibilidad de primera clase con conversiones implícitas de estilo .NET.

Esta característica implementa F# RFC FS-1093.

Conversiones a tipo básico implícitas adicionales

F# 6 implementa conversiones a tipo básico implícitas adicionales. Por ejemplo, en F# 5 y versiones anteriores, se necesitaban conversiones a tipo básico para la expresión de retorno al implementar una función en la que las expresiones tenían subtipos diferentes en distintas ramas, incluso cuando había una anotación de tipo. Considere el siguiente código de F# 5:

open System
open System.IO

let findInputSource () : TextReader =
    if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
        // On Monday a TextReader
        Console.In
    else
        // On other days a StreamReader
        File.OpenText("path.txt") :> TextReader

Aquí las ramas condicionales procesan TextReader y StreamReader, respectivamente, y la conversión a tipo básico se agrega para que ambas ramas tengan el tipo StreamReader. En F# 6, estas conversiones a tipo básico se agregan automáticamente. Significa que el código es más sencillo:

let findInputSource () : TextReader =
    if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
        // On Monday a TextReader
        Console.In
    else
        // On other days a StreamReader
        File.OpenText("path.txt")

Opcionalmente, puede habilitar la advertencia /warnon:3388 para mostrar una advertencia en cada punto en que se usa una conversión a tipo básico implícita adicional, como se describe en Advertencias opcionales para conversiones implícitas.

Conversiones de enteros implícitas

En F# 6, los enteros de 32 bits se amplían a enteros de 64 bits cuando se conocen ambos tipos. Por ejemplo, considere una forma de API típica:

type Tensor(…) =
    static member Create(sizes: seq<int64>) = Tensor(…)

En F# 5, se deben usar literales enteros para int64:

Tensor.Create([100L; 10L; 10L])

o

Tensor.Create([int64 100; int64 10; int64 10])

En F# 6, la ampliación se produce automáticamente de int32 a int64, de int32 a nativeint y de int32 a double, cuando se conocen tanto el tipo de origen como el de destino durante la inferencia de tipos. Por lo tanto, en casos como los ejemplos anteriores, se pueden usar literales int32:

Tensor.Create([100; 10; 10])

A pesar de este cambio, F# sigue usando la ampliación explícita de tipos numéricos en la mayoría de los casos. Por ejemplo, el ampliación implícita no se aplica a otros tipos numéricos, como int8 o int16, o de float32 a float64, o cuando se desconoce el tipo de origen o de destino. Opcionalmente, puede habilitar la advertencia /warnon:3389 para mostrar una advertencia en cada punto en que se usa una ampliación numérica implícita, como se describe en Advertencias opcionales para conversiones implícitas.

Compatibilidad de primera clase con conversiones implícitas de estilo .NET

En F# 6, las conversiones "op_Implicit" de .NET se aplican automáticamente en el código de F# al llamar a métodos. Por ejemplo, en F# 5 era necesario usar XName.op_Implicit al trabajar con las API de .NET para XML:

open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants(XName.op_Implicit "Item")

En F# 6, las conversiones op_Implicit se aplican automáticamente para las expresiones de argumentos cuando los tipos están disponibles para la expresión de origen y el tipo de destino:

open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants("Item")

Opcionalmente, puede habilitar la advertencia /warnon:3395 para mostrar una advertencia en cada punto en que se usa una ampliación de conversiones op_Implicit, como se describe en Advertencias opcionales para conversiones implícitas.

Nota

En la primera versión de F# 6, este número de advertencia era /warnon:3390. Debido a un conflicto, el número de advertencia se actualizó posteriormente a /warnon:3395.

Advertencias opcionales para conversiones implícitas

Las conversiones implícitas y dirigidas por tipos pueden interactuar deficientemente con la inferencia de tipos y conducir a código que es más difícil de entender. Por este motivo, existen algunas mitigaciones para ayudar a garantizar que esta característica no se use en el código de F#. En primer lugar, tanto el tipo de origen como el de destino deben ser muy conocidos, sin ambigüedad ni inferencia de tipos adicionales. En segundo lugar, se pueden activar las advertencias de participación para notificar cualquier uso de conversiones implícitas, con una advertencia activada de forma predeterminada:

  • /warnon:3388 (conversión a tipo básico implícita adicional)
  • /warnon:3389 (ampliación numérica implícita)
  • /warnon:3391 (op_Implicit en argumentos que no son de método, activada de forma predeterminada)
  • /warnon:3395 (op_Implicit en argumentos de método)

Si el equipo quiere prohibir todos los usos de conversiones implícitas, también puede especificar /warnaserror:3388, /warnaserror:3389/warnaserror:3391 y /warnaserror:3395.

Formato de números binarios

F# 6 agrega el patrón %B a los especificadores de formato disponibles para formatos de números binarios. Considere el siguiente código de F#:

printf "%o" 123
printf "%B" 123

Este código imprime la siguiente salida:

173
1111011

Esta característica implementa F# RFC FS-1100.

Descartes en enlaces de uso

F# 6 permite usar _ en un enlace use, por ejemplo:

let doSomething () =
    use _ = System.IO.File.OpenText("input.txt")
    printfn "reading the file"

Esta característica implementa F# RFC FS-1102.

InlineIfLambda

El compilador de F# incluye un optimizador que realiza la inserción de código. En F# 6 hemos agregado una nueva característica declarativa que permite que el código indique opcionalmente que, si un argumento se determina como una función lambda, ese argumento siempre debe insertarse en los sitios de llamada.

Por ejemplo, considera la siguiente función iterateTwice para atravesar una matriz:

let inline iterateTwice ([<InlineIfLambda>] action) (array: 'T[]) =
    for j = 0 to array.Length-1 do
        action array[j]
    for j = 0 to array.Length-1 do
        action array[j]

Si el sitio de llamada es:

let arr = [| 1.. 100 |]
let mutable sum = 0
arr  |> iterateTwice (fun x ->
    sum <- sum + x)

Después de insertar y otras optimizaciones, el código se convierte en:

let arr = [| 1.. 100 |]
let mutable sum = 0
for j = 0 to arr.Length-1 do
    sum <- sum + arr[j]
for j = 0 to arr.Length-1 do
    sum <- sum + arr[j]

A diferencia de las versiones anteriores de F#, esta optimización se aplica independientemente del tamaño de la expresión lambda implicada. Esta característica también se puede usar para implementar la anulación de bucles y transformaciones similares de forma más confiable.

Se puede activar una advertencia de participación (/warnon:3517, desactivada de forma predeterminada) para indicar los lugares en el código donde los argumentos InlineIfLambda no están enlazados a expresiones lambda en los sitios de llamada. En situaciones normales, esta advertencia no debe estar habilitada. Sin embargo, en ciertos tipos de programación de alto rendimiento, puede ser útil asegurarse de que todo el código está insertado y acoplado.

Esta característica implementa F# RFC FS-1098.

Código reanudable

La compatibilidad de task {…} con F# 6 se basa en una base denominada código reanudableRFC FS-1087. El código reanudable es una característica técnica que se puede usar para crear muchas clases de máquinas de estado asincrónicas y de alto rendimiento.

Funciones de colección adicionales

FSharp.Core 6.0.0 agrega cinco nuevas operaciones a las funciones de colección principal. Estas funciones son las siguientes:

  • List/Array/Seq.insertAt
  • List/Array/Seq.removeAt
  • List/Array/Seq.updateAt
  • List/Array/Seq.insertManyAt
  • List/Array/Seq.removeManyAt

Todas estas funciones realizan operaciones de copia y actualización en el tipo o secuencia de colección correspondiente. Este tipo de operación es una forma de "actualización funcional". Para ejemplos de uso de estas funciones, consulte la documentación correspondiente, por ejemplo, List.insertAt.

Por ejemplo, considere el modelo, el mensaje y la lógica de actualización para una sencilla aplicación "Lista de tareas pendientes" escrita en el estilo Elmish. Aquí el usuario interactúa con la aplicación, genera mensajes y la función update procesa estos mensajes, lo que genera un nuevo modelo:

type Model =
    { ToDo: string list }

type Message =
    | InsertToDo of index: int * what: string
    | RemoveToDo of index: int
    | LoadedToDos of index: int * what: string list

let update (model: Model) (message: Message) =
    match message with
    | InsertToDo (index, what) ->
        { model with ToDo = model.ToDo |> List.insertAt index what }
    | RemoveToDo index ->
        { model with ToDo = model.ToDo |> List.removeAt index }
    | LoadedToDos (index, what) ->
        { model with ToDo = model.ToDo |> List.insertManyAt index what }

Con estas nuevas funciones, la lógica es clara y sencilla y solo se basa en datos inmutables.

Esta característica implementa F# RFC FS-1113.

El mapa tiene claves y valores

En FSharp.Core 6.0.0, el tipo Map admite ahora las propiedades Keys y Values. Estas propiedades no copian la colección subyacente.

Esta característica se documenta en F# RFC FS-1113.

Elementos intrínsecos adicionales para NativePtr

FSharp.Core 6.0.0 agrega nuevos elementos intrínsecos al módulo NativePtr:

  • NativePtr.nullPtr
  • NativePtr.isNullPtr
  • NativePtr.initBlock
  • NativePtr.clear
  • NativePtr.copy
  • NativePtr.copyBlock
  • NativePtr.ofILSigPtr
  • NativePtr.toILSigPtr

Igual que otras funciones de NativePtr, estas funciones están insertadas y su uso emite advertencias a menos que se use /nowarn:9. El uso de estas funciones está restringido a tipos no administrados.

Esta característica se documenta en F# RFC FS-1109.

Tipos numéricos adicionales con anotaciones de unidad

En F# 6, los siguientes tipos o alias de abreviatura de tipo admiten anotaciones de unidad de medida. Las nuevas adiciones se muestran en negrita:

Alias F# Tipo CLR
float32/single System.Single
float/double System.Double
decimal System.Decimal
sbyte/int8 System.SByte
int16 System.Int16
int/int32 System.Int32
int64 System.Int64
byte/uint8 System.Byte
uint16 System.UInt16
uint/uint32 System.UInt32
uint64 System.UIn64
nativeint System.IntPtr
unativeint System.UIntPtr

Por ejemplo, puede anotar un entero sin signo de la siguiente manera:

[<Measure>]
type days

let better_age = 3u<days>

Esta característica se documenta en F# RFC FS-1091.

Advertencias informativas para operadores simbólicos usados con poca frecuencia

F# 6 agrega instrucciones flexibles que desnormalizan el uso de :=, !, incr y decr en F# 6 y versiones posteriores. El uso de estos operadores y funciones genera mensajes informativos que le piden que reemplace el código por el uso explícito de la propiedad Value.

En la programación de F#, las celdas de referencia se pueden usar para los registros mutables asignados al montón. Aunque son útiles ocasionalmente, rara vez son necesarias en la codificación moderna de F#, ya que se puede usar let mutable en su lugar. La biblioteca principal de F# incluye dos operadores, := y !, y dos funciones, incr y decr, específicamente relacionadas con las llamadas de referencia. La presencia de estos operadores hace que las celdas de referencia sean más importantes para la programación de F# de lo que deben ser, lo que requiere que todos los programadores de F# conozcan estos operadores. Además, el operador ! se puede confundir fácilmente con la operación not en C# y otros lenguajes, una fuente potencialmente sutil de errores al traducir código.

La razón de este cambio es reducir el número de operadores que el programador de F# necesita conocer y, por tanto, simplificar F# para principiantes.

Por ejemplo, considere el código de F# 5 siguiente:

let r = ref 0

let doSomething() =
    printfn "doing something"
    r := !r + 1

En primer lugar, las celdas de referencia rara vez son necesarias en la codificación moderna de F#, ya que normalmente se puede usar let mutable en su lugar:

let mutable r = 0

let doSomething() =
    printfn "doing something"
    r <- r + 1

Si usa celdas de referencia, F# 6 emite una advertencia informativa en la que se le pide que cambie la última línea a r.Value <- r.Value + 1 y que se vincule a instrucciones adicionales sobre el uso adecuado de las celdas de referencia.

let r = ref 0

let doSomething() =
    printfn "doing something"
    r.Value <- r.Value + 1

Estos mensajes no son advertencias; son "mensajes informativos" que se muestran en el IDE y en la salida del compilador. F# sigue siendo compatible con versiones anteriores.

Esta característica implementa F# RFC FS-1111.

Herramientas de F#: .NET 6 es el valor predeterminado para el scripting en Visual Studio

Si abre o ejecuta un script de F# (.fsx) en Visual Studio, se analizará y ejecutará el script de forma predeterminada mediante .NET 6 con la ejecución de 64 bits. Esta funcionalidad estaba en versión preliminar en las últimas versiones de Visual Studio 2019 y ahora está habilitada de forma predeterminada.

Para habilitar el scripting de .NET Framework, seleccione Herramientas>Opciones>Herramientas de F#>F# interactivo. Establezca Usar scripting de .NET Core en falso y, a continuación, reinicie la ventana F# interactivo. Esta configuración afecta tanto a la edición como a la ejecución de scripts. Para habilitar la ejecución de 32 bits para el scripting de .NET Framework, establezca también F# interactivo de 64 bits en falso. No hay ninguna opción de 32 bits para el scripting de .NET Core.

Herramientas de F#: anclar la versión del SDK de los scripts de F#

Si ejecuta un script mediante dotnet fsi en un directorio que contiene un archivo global.json con una configuración del SDK de .NET, se usará la versión enumerada del SDK de .NET para ejecutar y editar el script. Esta característica está disponible en las últimas versiones de F# 5.

Por ejemplo, supongamos que hay un script en un directorio con el siguiente archivo global.json que especifica una directiva de versión del SDK de .NET:

{
  "sdk": {
    "version": "5.0.200",
    "rollForward": "minor"
  }
}

Si ahora ejecuta el script mediante dotnet fsi, desde este directorio, se respetará la versión del SDK. Se trata de una característica eficaz que permite "bloquear" el SDK usado para compilar, analizar y ejecutar los scripts.

Si abre y edita el script en Visual Studio y otros IDE, las herramientas respetarán esta configuración al analizar y comprobar el script. Si no se encuentra el SDK, deberá instalarlo en la máquina de desarrollo.

En Linux y otros sistemas Unix, puede combinarlo con un shebang para especificar también una versión del lenguaje para la ejecución directa del script. Un shebang simple para script.fsx es:

#!/usr/bin/env -S dotnet fsi

printfn "Hello, world"

Ahora, el script se puede ejecutar directamente con script.fsx. Puede combinarlo con una versión específica no predeterminada del lenguaje como esta:

#!/usr/bin/env -S dotnet fsi --langversion:5.0

Nota

Esta configuración se omite mediante herramientas de edición, que analizarán el script suponiendo la versión más reciente del lenguaje.

Eliminación de características heredadas

Desde F# 2.0, algunas características heredadas en desuso han generado advertencias. El uso de estas características en F# 6 produce errores a menos que use /langversion:5.0 explícitamente. Las características que proporcionan errores son:

  • Varios parámetros genéricos con un nombre de tipo postfijo, por ejemplo (int, int) Dictionary. Se convierte en un error en F# 6. Debería usarse la sintaxis estándar Dictionary<int,int> en lugar de la anterior.
  • #indent "off". Se convierte en un error.
  • x.(expr). Se convierte en un error.
  • module M = struct … end . Se convierte en un error.
  • Uso de las entradas *.ml y *.mli. Se convierte en un error.
  • Uso de (*IF-CAML*) o (*IF-OCAML*). Se convierte en un error.
  • Uso de land, lor, lxor, lsl, lsr o asr como operadores de infijo. Estas son palabras clave de infijo en F# porque eran palabras clave de infijo en OCaml y no se definen en FSharp.Core. El uso de estas palabras clave emitirá una advertencia.

Esto implementa F# RFC FS-1114.