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.