Yang baru di F# 5

F# 5 menambahkan beberapa peningkatan pada bahasa pemrogram F# dan F# Interactive. Ini dirilis dengan .NET 5.

Anda dapat mengunduh SDK .NET terbaru dari halaman unduhan .NET.

Memulai

F# 5 tersedia di semua distribusi .NET Core dan peralatan Visual Studio. Untuk informasi selengkapnya, lihat Mulai menggunakan F# untuk mempelajari selengkapnya.

Referensi paket dalam skrip F#

F# 5 menghadirkan dukungan untuk referensi paket dalam skrip F# dengan sintaks #r "nuget:...". Sebagai contoh, pertimbangkan referensi paket berikut:

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

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

printfn $"{JsonConvert.SerializeObject o}"

Anda juga dapat menyediakan versi eksplisit setelah nama paket seperti ini:

#r "nuget: Newtonsoft.Json,11.0.1"

Referensi paket mendukung paket dengan dependensi asli, seperti ML.NET.

Referensi paket juga mendukung paket dengan persyaratan khusus tentang referensi dependen .dll. Misalnya, paket FParsec yang digunakan untuk mengharuskan pengguna memastikan secara manual bahwa dependen FParsecCS.dll direferensikan terlebih dahulu sebelum FParsec.dll direferensikan dalam F# Interactive. Ini tidak lagi diperlukan, dan Anda dapat mereferensikan paket seperti berikut:

#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"

Fitur ini menerapkan Peralatan F# RFC FST-1027. Untuk informasi selengkapnya tentang referensi paket, lihat tutorial F# Interactive.

Interpolasi string

String terinterpolasi F# cukup mirip dengan string terinterpolasi C# atau JavaScript, sehingga memungkinkan Anda menulis kode di "lubang" di dalam string literal. Berikut ini contoh dasarnya:

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

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

Namun, string terinterpolasi F# juga memungkinkan interpolasi berjenis, sama seperti fungsi sprintf, untuk menegakkan kalau ekspresi di dalam konteks terinterpolasi sesuai dengan jenis tertentu. Ini menggunakan penentu format yang sama.

let name = "Phillip"
let age = 29

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

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

Dalam contoh interpolasi berjenis sebelumnya, %s mengharuskan interpolasi berjenis string, sedangkan %d mengharuskan interpolasi menjadi integer.

Selain itu, ekspresi F# arbitrer apa pun (atau ekspresi) dapat ditempatkan di samping konteks interpolasi. Bahkan dimungkinkan untuk menulis ekspresi yang lebih rumit, seperti:

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]
}
"""

Meskipun kami tidak merekomendasikan untuk melakukan ini terlalu sering dalam praktik.

Fitur ini menerapkan F# RFC FS-1001.

Dukungan untuk nameof

F# 5 mendukung operator nameof, yang menyelesaikan simbol yang digunakannya dan menghasilkan namanya dalam sumber F#. Ini berguna dalam berbagai skenario, seperti pengelogan, dan melindungi pengelogan Anda dari perubahan kode sumber.

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}"

Baris terakhir akan memberikan pengecualian dan "month" akan ditampilkan dalam pesan kesalahan.

Anda dapat mengambil nama hampir setiap konstruksi F#:

module M =
    let f x = nameof x

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

Tiga penambahan akhir merupakan perubahan pada cara kerja operator: penambahan formulir nameof<'type-parameter> untuk parameter jenis generik, dan kemampuan untuk menggunakan nameof sebagai pola dalam ekspresi pencocokan pola.

Mengambil nama operator memberikan string sumbernya. Jika Anda memerlukan bentuk yang dikompilasi, gunakan nama operator yang dikompilasi:

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

Mengambil nama parameter jenis memerlukan sintaks yang agak berbeda:

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

Ini mirip dengan operator typeof<'T> dan typedefof<'T>.

F# 5 juga menambahkan dukungan untuk pola nameof yang dapat digunakan dalam ekspresi 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

Kode sebelumnya menggunakan 'nameof' alih-alih string literal dalam ekspresi pencocokan.

Fitur ini menerapkan F# RFC FS-1003.

Deklarasi jenis terbuka

F# 5 juga menambahkan dukungan untuk deklarasi jenis terbuka. Deklarasi jenis terbuka seperti membuka kelas statis di C#, kecuali dengan beberapa perbedaan sintaks dan beberapa perilaku yang agak berbeda agar sesuai dengan semantik F#.

Dengan deklarasi jenis terbuka, Anda dapat open jenis apa pun untuk mengekspos konten statis di dalamnya. Selain itu, Anda dapat open gabungan dan rekaman yang ditentukan F# untuk mengekspos kontennya. Misalnya, ini dapat berguna jika Anda memiliki gabungan yang ditentukan dalam modul dan ingin mengakses kasusnya, tetapi tidak ingin membuka keseluruhan modul.

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}"

Tidak seperti C#, ketika Anda open type pada dua jenis yang mengekspos anggota dengan nama yang sama, anggota dari jenis terakhir yang di-open membayangi nama lain. Ini konsisten dengan semantik F# di sekeliling bayangan yang sudah ada.

Fitur ini menerapkan F# RFC FS-1068.

Perilaku pemotongan konsisten untuk jenis data bawaan

Perilaku untuk memotong jenis data bawaan FSharp.Core (array, daftar, string, array 2D, array 3D, array 4D) yang digunakan agar menjadi tidak konsisten sebelum F# 5. Beberapa perilaku kasus tepi melemparkan pengecualian dan beberapa tidak. Di F# 5, semua jenis bawaan sekarang mengembalikan potongan kosong untuk potongan yang tidak mungkin dihasilkan:

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)]

Fitur ini menerapkan F# RFC FS-1077.

Potongan indeks tetap untuk array 3D dan 4D di FSharp.Core

F# 5 menghadirkan dukungan untuk pemotongan dengan indeks tetap dalam jenis array 3D dan 4D bawaan.

Untuk menggambarkan ini, pertimbangkan array 3D berikut:

z = 0

x\y 0 1
0 0 1
1 2 3

z = 1

x\y 0 1
0 4 5
1 6 7

Bagaimana jika Anda ingin mengekstrak potongan [| 4; 5 |] dari array? Ini menjadi sangat sederhana sekarang!

// 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]

Fitur ini menerapkan F# RFC FS-1077b.

Peningkatan kutipan F#

Kutipan kode F# sekarang memiliki kemampuan untuk mempertahankan informasi batasan jenis. Pertimbangkan contoh berikut:

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

Batasan yang dibuat oleh fungsi inline dipertahankan dalam kutipan kode. Formulir yang dikutip fungsi negate sekarang dapat dievaluasi.

Fitur ini menerapkan F# RFC FS-1071.

Ekspresi Komputasi Aplikatif

Ekspresi komputasi (CEs) digunakan saat ini untuk pemodelan "komputasi kontekstual", atau dalam terminologi yang lebih ramah pemrograman fungsional, komputasi monadik.

F# 5 memperkenalkan CEs aplikatif, yang menawarkan model komputasi yang berbeda. CEs aplikatif memungkinkan komputasi yang lebih efisien asalkan setiap komputasi independen, dan hasilnya terakumulasi di akhir. Ketika komputasi independen satu sama lain, setiap komputasi juga sederhananya dapat diparalelkan, memungkinkan pembuat CE untuk menulis pustaka yang lebih efisien. Manfaat ini dilengkapi dengan batasan, meskipun: komputasi yang bergantung pada nilai komputasi sebelumnya tidak diizinkan.

Contoh berikut menunjukkan CE aplikatif dasar untuk jenis Result tersebut.

// 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

Jika Anda adalah pembuat pustaka yang mengekspos CEs di pustaka-nya hari ini, ada beberapa pertimbangan tambahan yang perlu Anda ketahui.

Fitur ini menerapkan F# RFC FS-1063.

Antarmuka dapat diterapkan pada instansiasi generik yang berbeda

Anda sekarang dapat menerapkan antarmuka yang sama pada instansiasi generik yang berbeda:

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"

Fitur ini menerapkan F# RFC FS-1031.

Pemakaian anggota antarmuka default

F# 5 memungkinkan Anda memakai antarmuka dengan penerapan default.

Pertimbangkan antarmuka yang ditentukan dalam C# seperti ini:

using System;

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

Anda dapat memakainya di F# melalui salah satu cara standar untuk penerapan antarmuka:

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}"

Ini memungkinkan Anda memanfaatkan kode C# dan komponen .NET yang ditulis dalam C# modern dengan aman ketika kode dan komponen tersebut mengharapkan pengguna untuk dapat memakai penerapan default.

Fitur ini menerapkan F# RFC FS-1074.

Interop yang disederhanakan dengan jenis nilai yang dapat diubah ke null

Jenis (nilai) yang dapat diubah ke null (disebut Jenis Nullable secara historis) telah lama didukung oleh F#, tetapi berinteraksi dengannya secara tradisional agak menyulitkan karena Anda harus membangun pembungkus Nullable atau Nullable<SomeType> setiap kali Anda ingin meneruskan nilai. Sekarang kompilator akan mengonversi jenis nilai menjadi Nullable<ThatValueType> secara implisit jika jenis target cocok. Kode berikut sekarang dimungkinkan:

#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")))

Fitur ini menerapkan F# RFC FS-1075.

Pratinjau: indeks terbalik

F# 5 juga memperkenalkan pratinjau untuk memungkinkan indeks terbalik. Sintaksnya adalah ^idx. Berikut adalah cara agar Anda bisa mendapatkan nilai elemen 1 dari akhir daftar:

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

Anda juga dapat menentukan indeks terbalik untuk jenis Anda sendiri. Untuk melakukannya, Anda harus menerapkan metode berikut:

GetReverseIndex: dimension: int -> offset: int

Berikut adalah contoh untuk jenis 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

Fitur ini menerapkan F# RFC FS-1076.

Pratinjau: kelebihan beban kata kunci kustom di dalam ekspresi komputasi

Ekspresi komputasi adalah fitur yang kuat untuk pustaka dan pembuat kerangka kerja. Ekspresi ini memungkinkan Anda untuk meningkatkan ekspresi komponen Anda secara signifikan dengan membiarkan Anda menentukan anggota terkenal dan membentuk DSL untuk domain tempat Anda bekerja.

F# 5 menambahkan dukungan pratinjau untuk operasi kelebihan beban kustom dalam Ekspresi Komputasi. Ini memungkinkan kode berikut ditulis dan dipakai:

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)
    }

Sebelum perubahan ini, Anda dapat menulis jenis InputBuilder apa adanya, tetapi Anda tidak dapat menggunakannya seperti yang digunakan dalam contoh. Karena kelebihan beban, parameter opsional, dan jenis System.ParamArray sekarang diizinkan, semuanya hanya berfungsi seperti yang Anda harapkan.

Fitur ini menerapkan F# RFC FS-1056.