Novedades de F# 5

F# 5 agrega varias mejoras al lenguaje F# y a F# interactivo. Se publicó con .NET 5.

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

Introducción

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

Referencias de paquetes en scripts de F#

F# 5 ofrece compatibilidad con referencias de paquetes en scripts de F# con la sintaxis #r "nuget:...". Por ejemplo, considere la siguiente referencia de paquete:

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

let o = {| X = 2; Y = "Hello" |}

printfn $"{JsonConvert.SerializeObject o}"

También puede proporcionar una versión explícita después del nombre del paquete de la manera siguiente:

#r "nuget: Newtonsoft.Json,11.0.1"

Las referencias de paquete admiten paquetes con dependencias nativas, como ML.NET.

Las referencias de paquete también admiten paquetes con requisitos especiales sobre cómo hacer referencia a elementos .dll dependientes. Por ejemplo, el paquete FParsec solía requerir que los usuarios se asegurasen manualmente de que se había hecho referencia a su elemento FParsecCS.dll dependiente antes de hacer referencia al elemento FParsec.dll en F# interactivo. Esto ya no es necesario y se puede hacer referencia al paquete de la manera siguiente:

#r "nuget: FParsec"

open FParsec

let test p str =
    match run p str with
    | Success(result, _, _)   -> printfn $"Success: {result}"
    | Failure(errorMsg, _, _) -> printfn $"Failure: {errorMsg}"

test pfloat "1.234"

Esta característica implementa las herramientas de F# RFC FST-1027. Para obtener más información sobre las referencias de paquete, consulte el tutorial F# interactivo.

Interpolación de cadenas

Las cadenas interpoladas de F# son bastante similares a las de C# o JavaScript, en el sentido de que permiten escribir código en "agujeros" dentro de un literal de cadena. Este es un ejemplo básico:

let name = "Phillip"
let age = 29
printfn $"Name: {name}, Age: {age}"

printfn $"I think {3.0 + 0.14} is close to {System.Math.PI}!"

Las cadenas interpoladas de F# también permiten interpolaciones con tipo, igual que la función sprintf, para exigir que una expresión dentro de un contexto interpolado se ajuste a un tipo determinado. Se usan los mismos especificadores de formato.

let name = "Phillip"
let age = 29

printfn $"Name: %s{name}, Age: %d{age}"

// Error: type mismatch
printfn $"Name: %s{age}, Age: %d{name}"

En el ejemplo de interpolación con tipo anterior, %s requiere que la interpolación sea de tipo string, mientras que %d requiere que la interpolación sea integer.

Además, cualquier expresión (o expresiones) arbitraria de F# se puede colocar dentro de un contexto de interpolación. Incluso es posible escribir una expresión más complicada, como esta:

let str =
    $"""The result of squaring each odd item in {[1..10]} is:
{
    let square x = x * x
    let isOdd x = x % 2 <> 0
    let oddSquares xs =
        xs
        |> List.filter isOdd
        |> List.map square
    oddSquares [1..10]
}
"""

Aun así, no se recomienda hacerlo a menudo en la práctica.

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

Compatibilidad con nameof

F# 5 admite el operador nameof, que resuelve el símbolo para el que se usa y genera su nombre en el origen de F#. Esto es útil en varios escenarios, como el registro, y protege el registro contra los cambios en el código fuente.

let months =
    [
        "January"; "February"; "March"; "April";
        "May"; "June"; "July"; "August"; "September";
        "October"; "November"; "December"
    ]

let lookupMonth month =
    if (month > 12 || month < 1) then
        invalidArg (nameof month) (sprintf "Value passed in was %d." month)

    months[month-1]

printfn $"{lookupMonth 12}"
printfn $"{lookupMonth 1}"
printfn $"{lookupMonth 13}"

La última línea producirá una excepción y se mostrará "month" en el mensaje de error.

Puede tomar un nombre de casi todas las construcciones de F#:

module M =
    let f x = nameof x

printfn $"{M.f 12}"
printfn $"{nameof M}"
printfn $"{nameof M.f}"

Las tres últimas adiciones son cambios en el funcionamiento de los operadores: la inclusión de la forma nameof<'type-parameter> para los parámetros de tipo genérico y la capacidad de usar nameof como patrón en una expresión de coincidencia de patrones.

Al tomar un nombre de un operador, se proporciona su cadena de origen. Si necesita la forma compilada, use el nombre compilado de un operador:

nameof(+) // "+"
nameof op_Addition // "op_Addition"

Para tomar el nombre de un parámetro de tipo se requiere una sintaxis ligeramente diferente:

type C<'TType> =
    member _.TypeName = nameof<'TType>

Esto es similar a los operadores typeof<'T> y typedefof<'T>.

F# 5 también agrega compatibilidad con un patrón nameof que se puede usar en expresiones match:

[<Struct; IsByRefLike>]
type RecordedEvent = { EventType: string; Data: ReadOnlySpan<byte> }

type MyEvent =
    | AData of int
    | BData of string

let deserialize (e: RecordedEvent) : MyEvent =
    match e.EventType with
    | nameof AData -> AData (JsonSerializer.Deserialize<int> e.Data)
    | nameof BData -> BData (JsonSerializer.Deserialize<string> e.Data)
    | t -> failwithf "Invalid EventType: %s" t

El código anterior usa "nameof" en lugar del literal de cadena en la expresión de coincidencia.

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

Declaraciones de tipo abierto

F# 5 también agrega compatibilidad con declaraciones de tipo abierto. Una declaración de tipo abierto es semejante a abrir una clase estática en C#, excepto por algunas diferencias en la sintaxis y en el comportamiento para ajustarse a la semántica de F#.

Con las declaraciones de tipo abierto, es posible abrir con open cualquier tipo para exponer el contenido estático de su interior. También se puede abrir con open uniones y registros definidos por F# para exponer su contenido. Por ejemplo, esto puede ser útil si tiene una unión definida en un módulo y quiere acceder a sus casos, pero no le interesa abrir todo el módulo.

open type System.Math

let x = Min(1.0, 2.0)

module M =
    type DU = A | B | C

    let someOtherFunction x = x + 1

// Open only the type inside the module
open type M.DU

printfn $"{A}"

A diferencia de C#, donde se usa open type en dos tipos que exponen un miembro con el mismo nombre, el miembro del último tipo que se abre con open reemplaza el otro nombre. Esto es coherente con la semántica de F# que ya existe con respecto al reemplazamiento.

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

Comportamiento de segmentación coherente para tipos de datos integrados

El comportamiento al segmentar los tipos de datos de FSharp.Core integrados (matriz, lista, cadena, matriz 2D, matriz 3D, matriz 4D) solía no ser coherente antes de F# 5. Algunos casos extremos del comportamiento generaban una excepción, mientras que otros no lo hacían. En F# 5, todos los tipos integrados ahora devuelven segmentos vacíos en el caso de los segmentos que son imposibles de generar:

let l = [ 1..10 ]
let a = [| 1..10 |]
let s = "hello!"

// Before: would return empty list
// F# 5: same
let emptyList = l[-2..(-1)]

// Before: would throw exception
// F# 5: returns empty array
let emptyArray = a[-2..(-1)]

// Before: would throw exception
// F# 5: returns empty string
let emptyString = s[-2..(-1)]

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

Segmentos de índice fijo para matrices 3D y 4D en FSharp.Core

F# 5 ofrece compatibilidad con la segmentación con un índice fijo en los tipos de matriz 3D y 4D integrados.

Para ilustrarlo, considere la siguiente matriz 3D:

z = 0

x\y 0 1
0 0 1
1 2 3

z = 1

x\y 0 1
0 4 5
1 6 7

¿Qué ocurre si quiere extraer el segmento [| 4; 5 |] de la matriz? Ahora esto es muy sencillo.

// First, create a 3D array to slice

let dim = 2
let m = Array3D.zeroCreate<int> dim dim dim

let mutable count = 0

for z in 0..dim-1 do
    for y in 0..dim-1 do
        for x in 0..dim-1 do
            m[x,y,z] <- count
            count <- count + 1

// Now let's get the [4;5] slice!
m[*, 0, 1]

Esta característica implementa F# RFC FS-1077b.

Mejoras en las expresiones de código delimitadas de F#

Las expresiones de código delimitadas de F# ahora tienen la capacidad de conservar la información de restricción de tipo. Considere el ejemplo siguiente:

open FSharp.Linq.RuntimeHelpers

let eval q = LeafExpressionConverter.EvaluateQuotation q

let inline negate x = -x
// val inline negate: x: ^a ->  ^a when  ^a : (static member ( ~- ) :  ^a ->  ^a)

<@ negate 1.0 @>  |> eval

La restricción generada por la función inline se conserva en la expresión de código delimitada. Ahora se puede evaluar la forma delimitada de la función negate.

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

Expresiones de cálculo aplicativas

Hoy en día, se usan expresiones de cálculo para modelar "cálculos contextuales" o, dicho con palabras más adecuadas para la programación funcional, cálculos monádicos.

F# 5 presenta expresiones de cálculo aplicativas que ofrecen un modelo computacional diferente. Las expresiones de cálculo aplicativas permiten cálculos más eficaces, siempre y cuando cada cálculo sea independiente, y sus resultados se acumulan al final. Cuando los cálculos son independientes entre sí, también se pueden paralelizar de forma trivial, lo que permite a los autores de las expresiones de cálculo escribir bibliotecas más eficaces. Aun así, esta ventaja conlleva una restricción: no se permiten cálculos que dependan de valores calculados previamente.

En el ejemplo siguiente se muestra una expresión de cálculo aplicativa básica para el tipo Result.

// First, define a 'zip' function
module Result =
    let zip x1 x2 =
        match x1,x2 with
        | Ok x1res, Ok x2res -> Ok (x1res, x2res)
        | Error e, _ -> Error e
        | _, Error e -> Error e

// Next, define a builder with 'MergeSources' and 'BindReturn'
type ResultBuilder() =
    member _.MergeSources(t1: Result<'T,'U>, t2: Result<'T1,'U>) = Result.zip t1 t2
    member _.BindReturn(x: Result<'T,'U>, f) = Result.map f x

let result = ResultBuilder()

let run r1 r2 r3 =
    // And here is our applicative!
    let res1: Result<int, string> =
        result {
            let! a = r1
            and! b = r2
            and! c = r3
            return a + b - c
        }

    match res1 with
    | Ok x -> printfn $"{nameof res1} is: %d{x}"
    | Error e -> printfn $"{nameof res1} is: {e}"

let printApplicatives () =
    let r1 = Ok 2
    let r2 = Ok 3 // Error "fail!"
    let r3 = Ok 4

    run r1 r2 r3
    run r1 (Error "failure!") r3

Si es el autor de una biblioteca que expone actualmente expresiones de cálculo, debe tener en cuenta algunas consideraciones adicionales.

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

Las interfaces se pueden implementar en creaciones de instancias genéricas diferentes

Ahora puede implementar la misma interfaz en creaciones de instancias genéricas diferentes:

type IA<'T> =
    abstract member Get : unit -> 'T

type MyClass() =
    interface IA<int> with
        member x.Get() = 1
    interface IA<string> with
        member x.Get() = "hello"

let mc = MyClass()
let iaInt = mc :> IA<int>
let iaString = mc :> IA<string>

iaInt.Get() // 1
iaString.Get() // "hello"

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

Consumo de miembros de interfaz predeterminados

F# 5 permite consumir interfaces con implementaciones predeterminadas.

Considere una interfaz definida en C# de la manera siguiente:

using System;

namespace CSharp
{
    public interface MyDim
    {
        public int Z => 0;
    }
}

Puede consumirla en F# mediante cualquiera de los medios estándar para implementar una interfaz:

open CSharp

// You can implement the interface via a class
type MyType() =
    member _.M() = ()

    interface MyDim

let md = MyType() :> MyDim
printfn $"DIM from C#: %d{md.Z}"

// You can also implement it via an object expression
let md' = { new MyDim }
printfn $"DIM from C# but via Object Expression: %d{md'.Z}"

Esto le permite usar de forma segura el código de C# y los componentes de .NET escritos en C# moderno cuando esperan que los usuarios consuman una implementación predeterminada.

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

Interoperabilidad simplificada con tipos que aceptan valores NULL

Los tipos que aceptan valores NULL son compatibles con F# desde hace mucho tiempo, pero la interacción con ellos ha sido tradicionalmente un problema, ya que era necesario construir un contenedor Nullable o Nullable<SomeType> cada vez que se quería pasar un valor. Ahora el compilador convertirá implícitamente un tipo de valor en Nullable<ThatValueType> si el tipo de destino coincide. El código siguiente ahora es posible:

#r "nuget: Microsoft.Data.Analysis"

open Microsoft.Data.Analysis

let dateTimes = PrimitiveDataFrameColumn<DateTime>("DateTimes")

// The following line used to fail to compile
dateTimes.Append(DateTime.Parse("2019/01/01"))

// The previous line is now equivalent to this line
dateTimes.Append(Nullable<DateTime>(DateTime.Parse("2019/01/01")))

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

Versión preliminar: índices inversos

F# 5 también presenta una versión preliminar para permitir los índices inversos. La sintaxis es ^idx. Aquí se muestra cómo se puede obtener el valor del elemento 1 del final de una lista:

let xs = [1..10]

// Get element 1 from the end:
xs[^1]

// From the end slices

let lastTwoOldStyle = xs[(xs.Length-2)..]

let lastTwoNewStyle = xs[^1..]

lastTwoOldStyle = lastTwoNewStyle // true

También puede definir índices inversos para sus propios tipos. Para ello, deberá implementar el método siguiente:

GetReverseIndex: dimension: int -> offset: int

Este es un ejemplo del tipo Span<'T>:

open System

type Span<'T> with
    member sp.GetSlice(startIdx, endIdx) =
        let s = defaultArg startIdx 0
        let e = defaultArg endIdx sp.Length
        sp.Slice(s, e - s)

    member sp.GetReverseIndex(_, offset: int) =
        sp.Length - offset

let printSpan (sp: Span<int>) =
    let arr = sp.ToArray()
    printfn $"{arr}"

let run () =
    let sp = [| 1; 2; 3; 4; 5 |].AsSpan()

    // Pre-# 5.0 slicing on a Span<'T>
    printSpan sp[0..] // [|1; 2; 3; 4; 5|]
    printSpan sp[..3] // [|1; 2; 3|]
    printSpan sp[1..3] // |2; 3|]

    // Same slices, but only using from-the-end index
    printSpan sp[..^0] // [|1; 2; 3; 4; 5|]
    printSpan sp[..^2] // [|1; 2; 3|]
    printSpan sp[^4..^2] // [|2; 3|]

run() // Prints the same thing twice

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

Versión preliminar: sobrecargas de palabras clave personalizadas en expresiones de cálculo

Las expresiones de cálculo son una característica eficaz para los autores de bibliotecas y marcos. Mejoran considerablemente la expresividad de los componentes al permitirle definir miembros conocidos y formar un DSL para el dominio en el que trabaja.

F# 5 agrega compatibilidad con la versión preliminar para sobrecargar operaciones personalizadas en las expresiones de cálculo. Permite escribir y consumir el código siguiente:

open System

type InputKind =
    | Text of placeholder:string option
    | Password of placeholder: string option

type InputOptions =
  { Label: string option
    Kind : InputKind
    Validators : (string -> bool) array }

type InputBuilder() =
    member t.Yield(_) =
      { Label = None
        Kind = Text None
        Validators = [||] }

    [<CustomOperation("text")>]
    member this.Text(io, ?placeholder) =
        { io with Kind = Text placeholder }

    [<CustomOperation("password")>]
    member this.Password(io, ?placeholder) =
        { io with Kind = Password placeholder }

    [<CustomOperation("label")>]
    member this.Label(io, label) =
        { io with Label = Some label }

    [<CustomOperation("with_validators")>]
    member this.Validators(io, [<ParamArray>] validators) =
        { io with Validators = validators }

let input = InputBuilder()

let name =
    input {
    label "Name"
    text
    with_validators
        (String.IsNullOrWhiteSpace >> not)
    }

let email =
    input {
    label "Email"
    text "Your email"
    with_validators
        (String.IsNullOrWhiteSpace >> not)
        (fun s -> s.Contains "@")
    }

let password =
    input {
    label "Password"
    password "Must contains at least 6 characters, one number and one uppercase"
    with_validators
        (String.exists Char.IsUpper)
        (String.exists Char.IsDigit)
        (fun s -> s.Length >= 6)
    }

Antes de este cambio, el tipo InputBuilder se podía escribir tal cual, pero no se podía usar como en el ejemplo. Dado que se permiten las sobrecargas, los parámetros opcionales y ahora los tipos System.ParamArray, todo funciona de la manera esperada.

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