Novedades de F# 5
F# 5 agrega varias mejoras al lenguaje F# y F# interactivo. Se ha publicado 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 y herramientas Visual Studio .NET Core. Para más información, consulte Introducción a F# para obtener más información.
Referencias de paquetes en scripts de F#
F# 5 ofrece compatibilidad con referencias de paquete en scripts de F# con #r "nuget:..." sintaxis. 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 siguiente manera:
#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 la referencia a .dll dependientes. Por ejemplo, el paquete FParsec usado para requerir que los usuarios se aseguren manualmente de que primero se hace referencia a su dependiente antes de que se haga referencia FParsecCS.dll FParsec.dll a F# interactivo. Esto ya no es necesario y puede hacer referencia al paquete como se muestra a continuación:
#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 paquetes.
Interpolación de cadenas
Las cadenas interpoladas de F# son bastante similares a las cadenas interpoladas de C# o JavaScript, ya que permiten escribir código en "huecos" 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}!"
Sin embargo, las cadenas interpoladas de F# también permiten interpolaciones con tipo, al igual que la función , para exigir que una expresión dentro de un contexto interpolado se ajuste sprintf a un tipo determinado. Usa 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, requiere que la interpolación sea de tipo , mientras que requiere que %s string la %d interpolación sea . integer
Además, cualquier expresión arbitraria de F# (o expresiones) se puede colocar en el lado de un contexto de interpolación. Incluso es posible escribir una expresión más complicada, como la siguiente:
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]
}
"""
Aunque no se recomienda hacer esto demasiado en la práctica.
Esta característica implementa F# RFC FS-1001.
Compatibilidad con nameof
F# 5 admite el operador , que resuelve el símbolo para el que se usa y genera su nameof nombre en el origen de F#. Esto es útil en varios escenarios, como el registro, y protege el registro frente a 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 "month" se mostrará 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}"
Tres adiciones finales son cambios en el modo en que funcionan los operadores: la adición del formulario para parámetros de tipo genérico y la capacidad de usar como patrón en una expresión de coincidencia nameof<'type-parameter> nameof de patrón.
Al tomar un nombre de un operador, se proporciona su cadena de origen. Si necesita el formulario compilado, use el nombre compilado de un operador:
nameof(+) // "+"
nameof op_Addition // "op_Addition"
La toma del nombre de un parámetro de tipo requiere una sintaxis ligeramente diferente:
type C<'TType> =
member _.TypeName = nameof<'TType>
Esto es similar a los typeof<'T> operadores typedefof<'T> y .
F# 5 también agrega compatibilidad con nameof un patrón que se puede usar en match expresiones:
[<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 match.
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 como abrir una clase estática en C#, excepto con alguna sintaxis diferente y algún comportamiento ligeramente diferente para ajustarse a la semántica de F#.
Con las declaraciones de tipo abierto, puede open cualquier tipo para exponer contenido estático dentro de él. Además, puede usar uniones open 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 desea acceder a sus casos, pero no quiere 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#, cuando se encuentra en dos tipos que exponen un miembro con el mismo nombre, el miembro del último tipo que se está open type open ed sombra el otro nombre. Esto es coherente con la semántica de F# en torno al sombreado que ya existe.
Esta característica implementa F# RFC FS-1068.
Comportamiento coherente de la licación para tipos de datos integrados
Comportamiento para cortar los tipos de datos integrados FSharp.Core (matriz, lista, cadena, matriz 2D, matriz 3D, matriz 4D) que no era coherente antes de F# 5. Algunos comportamientos de casos perimetrales han producido una excepción y otros no. En F# 5, todos los tipos integrados ahora devuelven segmentos vacíos para 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 para la selección con un índice fijo en los tipos de matriz 3D y 4D integrados.
Para ilustrar esto, 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 quisiera extraer el segmento [| 4; 5 |] de la matriz? Esto ahora 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 comillas de F#
Las comillas de código de F# ahora tienen la capacidad de conservar la información de restricción de tipos. 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 inline función se conserva en la comilla de código. Ahora negate se puede evaluar el formulario entre comillas de la función.
Esta característica implementa F# RFC FS-1071.
Expresiones de cálculo aplicado
Las expresiones de cálculo (CE) se usan hoy en día para modelar "cálculos contextuales", o en terminología más funcional y fácil de programar, cálculos módicos.
F# 5 presenta CE aplicativos, que ofrecen un modelo de cálculo diferente. Los CE aplicados permiten cálculos más eficaces siempre que cada cálculo sea independiente y sus resultados se acumulen al final. Cuando los cálculos son independientes entre sí, también se pueden paralelizar trivialmente, lo que permite a los autores de ce escribir bibliotecas más eficaces. Sin embargo, esta ventaja tiene una restricción: no se permiten los cálculos que dependen de valores calculados previamente.
En el ejemplo siguiente se muestra una ce de aplicación básica para el Result tipo.
// 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 un autor de biblioteca que expone los CE en su biblioteca hoy en día, hay algunas consideraciones adicionales que debe tener en cuenta.
Esta característica implementa F# RFC FS-1063.
Las interfaces se pueden implementar en diferentes instancias genéricas
Ahora puede implementar la misma interfaz en diferentes instancias genéricas:
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 predeterminado
F# 5 permite consumir interfaces con implementaciones predeterminadas.
Considere una interfaz definida en C# como esta:
using System;
namespace CSharp
{
public interface MyDim
{
public int Z => 0;
}
}
Puede consumirlo en F# a través de 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 aprovechar de forma segura el código de C# y los componentes de .NET escritos en C# moderno cuando esperan que los usuarios puedan consumir una implementación predeterminada.
Esta característica implementa F# RFC FS-1074.
Interoperabilidad simplificada con tipos de valor que aceptan valores NULL
F# admite desde hace mucho tiempo tipos que aceptan valores NULL (tipos que aceptan valores NULL), pero la interacción con ellos tradicionalmente ha sido un poco complicado, ya que tendría que construir un contenedor o cada vez que quisiera pasar Nullable un Nullable<SomeType> valor. Ahora, el compilador convertirá implícitamente un tipo de valor en si Nullable<ThatValueType> el tipo de destino coincide. Ahora es posible el código siguiente:
#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 índices inversos. La sintaxis es ^idx. Aquí se muestra cómo puede un valor de elemento 1 desde el 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 Span<'T> tipo:
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. Permiten mejorar enormemente la expresividad de los componentes, ya que permiten definir miembros conocidos y formar un DSL para el dominio en el que está trabajando.
F# 5 agrega compatibilidad en versión preliminar para sobrecargar operaciones personalizadas en 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, podía escribir el tipo tal como está, pero no podía usarlo de la manera en que se InputBuilder usa en el ejemplo. Dado que se permiten sobrecargas, parámetros opcionales y tipos ahora, todo funciona como System.ParamArray se esperaría.
Esta característica implementa F# RFC FS-1056.