O que há de novo no F# 5
O F# 5 adiciona várias melhorias à linguagem F# e ao F# interativo. É lançado com o .NET 5.
Você pode baixar o SDK mais recente do .NET na página de downloads do .NET.
Começar agora
O F# 5 está disponível em todas as distribuições do .NET Core e nas ferramentas do Visual Studio. Para obter mais informações, consulte Introdução ao F# para saber mais.
Referências de pacote em scripts F#
F# 5 traz suporte para referências de pacote em scripts F# com #r "nuget:..."
sintaxe. Por exemplo, considere a seguinte referência de pacote:
#r "nuget: Newtonsoft.Json"
open Newtonsoft.Json
let o = {| X = 2; Y = "Hello" |}
printfn $"{JsonConvert.SerializeObject o}"
Você também pode fornecer uma versão explícita após o nome do pacote assim:
#r "nuget: Newtonsoft.Json,11.0.1"
As referências de pacotes suportam pacotes com dependências nativas, como ML.NET.
As referências de pacotes também suportam pacotes com requisitos especiais sobre referência a dependentes .dll
. Por exemplo, o pacote FParsec usado para exigir que os usuários garantam manualmente que seu dependente FParsecCS.dll
foi referenciado primeiro antes de FParsec.dll
ser referenciado em F# Interativo. Isso não é mais necessário e você pode fazer referência ao pacote da seguinte maneira:
#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"
Este recurso implementa F # Tooling RFC FST-1027. Para obter mais informações sobre referências de pacote, consulte o tutorial interativo do F#.
Interpolação de cadeias
As cadeias interpoladas F# são bastante semelhantes às cadeias interpoladas C# ou JavaScript, na medida em que permitem escrever código em "buracos" dentro de um literal de cadeia de caracteres. Eis um exemplo 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}!"
No entanto, as cadeias de caracteres interpoladas F# também permitem interpolações digitadas, assim como a sprintf
função, para impor que uma expressão dentro de um contexto interpolado esteja em conformidade com um tipo específico. Ele usa os mesmos 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}"
No exemplo de interpolação digitado anterior, o %s
requer que a interpolação seja do tipo string
, enquanto o requer que a %d
interpolação seja um integer
.
Além disso, qualquer expressão (ou expressões) F# arbitrária pode ser colocada ao lado de um contexto de interpolação. É até possível escrever uma expressão mais complicada, assim:
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]
}
"""
Embora não recomendemos fazer isso muito na prática.
Este recurso implementa F # RFC FS-1001.
Suporte para nameof
F# 5 suporta o nameof
operador, que resolve o símbolo para o qual está sendo usado e produz seu nome na fonte F#. Isso é útil em vários cenários, como o registro em log, e protege seu registro contra alterações no código-fonte.
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}"
A última linha lançará uma exceção e "mês" será mostrado na mensagem de erro.
Você pode usar um nome de quase todas as construções F#:
module M =
let f x = nameof x
printfn $"{M.f 12}"
printfn $"{nameof M}"
printfn $"{nameof M.f}"
Três adições finais são alterações na forma como os operadores trabalham: a adição do formulário para parâmetros de tipo genéricos e a capacidade de usar nameof
como um padrão em uma expressão de correspondência de nameof<'type-parameter>
padrão.
Tomar um nome de um operador dá sua cadeia de caracteres de origem. Se você precisar do formulário compilado, use o nome compilado de um operador:
nameof(+) // "+"
nameof op_Addition // "op_Addition"
Tomar o nome de um parâmetro de tipo requer uma sintaxe ligeiramente diferente:
type C<'TType> =
member _.TypeName = nameof<'TType>
Isto é semelhante ao typeof<'T>
e typedefof<'T>
operadores.
F# 5 também adiciona suporte para um nameof
padrão que pode ser usado em match
expressões:
[<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
O código anterior usa 'nameof' em vez da string literal na expressão de correspondência.
Este recurso implementa F # RFC FS-1003.
Declarações de tipo aberto
F# 5 também adiciona suporte para declarações de tipo aberto. Uma declaração de tipo aberta é como abrir uma classe estática em C#, exceto com alguma sintaxe diferente e algum comportamento ligeiramente diferente para ajustar a semântica do F#.
Com declarações de tipo abertas, você pode open
qualquer tipo para expor conteúdo estático dentro dele. Além disso, você pode open
definir uniões e registros em F# para expor seu conteúdo. Por exemplo, isso pode ser útil se você tiver uma união definida em um módulo e quiser acessar seus casos, mas não quiser abrir o módulo inteiro.
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}"
Ao contrário do C#, quando você open type
em dois tipos que expõem um membro com o mesmo nome, o membro do último tipo que está sendo open
ed sombreia o outro nome. Isso é consistente com a semântica do F# em torno do sombreamento que já existe.
Este recurso implementa F # RFC FS-1068.
Comportamento de fatiamento consistente para tipos de dados internos
O comportamento para fatiar os tipos de FSharp.Core
dados internos (matriz, lista, cadeia de caracteres, matriz 2D, matriz 3D, matriz 4D) costumava não ser consistente antes do F# 5. Alguns comportamentos extremos abriram uma exceção e outros não. No F# 5, todos os tipos internos agora retornam fatias vazias para fatias que são impossíveis de gerar:
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)]
Este recurso implementa F # RFC FS-1077.
Fatias de índice fixo para matrizes 3D e 4D no FSharp.Core
O F# 5 traz suporte para fatiamento com um índice fixo nos tipos de matriz 3D e 4D integrados.
Para ilustrar isso, considere a seguinte 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 |
E se você quisesse extrair a fatia [| 4; 5 |]
da matriz? Isto agora é muito simples!
// 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]
Este recurso implementa F # RFC FS-1077b.
Melhorias nas cotações do F#
As cotações de código F# agora têm a capacidade de reter informações de restrição de tipo. Considere o seguinte exemplo:
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
A restrição gerada pela inline
função é mantida na cotação do código. A negate
forma citada da função pode agora ser avaliada.
Este recurso implementa F # RFC FS-1071.
Expressões computacionais de aplicação
Expressões computacionais (CEs) são usadas hoje para modelar "computações contextuais", ou em terminologia mais funcional e amigável à programação, cálculos monádicos.
F# 5 introduz CEs aplicativos, que oferecem um modelo computacional diferente. Os CEs de aplicação permitem cálculos mais eficientes, desde que cada cálculo seja independente e os seus resultados sejam acumulados no final. Quando os cálculos são independentes uns dos outros, eles também são trivialmente paralelizáveis, permitindo que os autores do CE escrevam bibliotecas mais eficientes. No entanto, esse benefício tem uma restrição: cálculos que dependem de valores previamente computados não são permitidos.
O exemplo a seguir mostra um CE aplicativo básico para o 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
Se você é um autor de biblioteca que expõe CEs em sua biblioteca hoje, há algumas considerações adicionais que você precisa estar ciente.
Este recurso implementa F # RFC FS-1063.
As interfaces podem ser implementadas em diferentes instanciações genéricas
Agora você pode implementar a mesma interface em diferentes instanciações 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"
Este recurso implementa F # RFC FS-1031.
Consumo de membro da interface padrão
O F# 5 permite consumir interfaces com implementações padrão.
Considere uma interface definida em C# assim:
using System;
namespace CSharp
{
public interface MyDim
{
public int Z => 0;
}
}
Você pode consumi-lo em F# através de qualquer um dos meios padrão de implementação de uma interface:
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}"
Isso permite que você aproveite com segurança o código C# e os componentes .NET escritos em C# moderno quando eles esperam que os usuários possam consumir uma implementação padrão.
Este recurso implementa F # RFC FS-1074.
Interoperabilidade simplificada com tipos de valor anuláveis
Os tipos anuláveis (de valor) (chamados historicamente de Tipos anuláveis) têm sido suportados por F#, mas interagir com eles tem sido tradicionalmente um pouco problemático, já que você teria que construir um Nullable
ou Nullable<SomeType>
wrapper toda vez que quisesse passar um valor. Agora, o compilador irá converter implicitamente um tipo de valor em um Nullable<ThatValueType>
se o tipo de destino corresponder. O seguinte código agora é possível:
#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")))
Este recurso implementa F # RFC FS-1075.
Pré-visualização: índices inversos
O F# 5 também apresenta uma visualização para permitir índices reversos. A sintaxe é ^idx
. Veja como você pode obter um valor de elemento 1 a partir do final de uma 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
Você também pode definir índices reversos para seus próprios tipos. Para fazer isso, você precisará implementar o seguinte método:
GetReverseIndex: dimension: int -> offset: int
Aqui está um exemplo para o 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
Este recurso implementa F # RFC FS-1076.
Pré-visualização: sobrecargas de palavras-chave personalizadas em expressões computacionais
As expressões de computação são um recurso poderoso para autores de bibliotecas e estruturas. Eles permitem que você melhore muito a expressividade de seus componentes, permitindo que você defina membros conhecidos e forme uma DSL para o domínio em que está trabalhando.
O F# 5 adiciona suporte de visualização para sobrecarregar operações personalizadas em expressões de computação. Ele permite que o seguinte código seja escrito e consumido:
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 dessa alteração, você podia escrever o InputBuilder
tipo como ele é, mas não podia usá-lo da maneira como é usado no exemplo. Como sobrecargas, parâmetros opcionais e agora System.ParamArray
tipos são permitidos, tudo funciona como você esperaria.
Este recurso implementa F # RFC FS-1056.