Новые возможности F# 5
F# 5 добавляет несколько улучшений в язык F# и F# Interactive. Она выпущена с помощью .NET 5.
Вы можете скачать последний пакет SDK для .NET на странице скачиваемых файлов .NET.
Начать
F# 5 доступен во всех дистрибутивах .NET Core и инструментах Visual Studio. Дополнительные сведения см. в статье "Начало работы с F#" для получения дополнительных сведений.
Ссылки на пакеты в скриптах F#
F# 5 обеспечивает поддержку ссылок на пакеты в скриптах F# с синтаксисом #r "nuget:..."
. Например, рассмотрим следующую ссылку на пакет:
#r "nuget: Newtonsoft.Json"
open Newtonsoft.Json
let o = {| X = 2; Y = "Hello" |}
printfn $"{JsonConvert.SerializeObject o}"
Вы также можете указать явную версию после имени пакета следующим образом:
#r "nuget: Newtonsoft.Json,11.0.1"
Ссылки на пакеты поддерживают пакеты с собственными зависимостями, например ML.NET.
Ссылки на пакеты также поддерживают пакеты с особыми требованиями к зависимостям .dll
ссылок. Например, пакет FParsec , используемый для того, чтобы пользователи вручную обеспечили ссылку на зависимый FParsecCS.dll
пакет, прежде чем FParsec.dll
ссылался в F# Interactive. Это больше не требуется, и вы можете ссылаться на пакет следующим образом:
#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"
Эта функция реализует F# Tooling RFC FST-1027. Дополнительные сведения о ссылках на пакеты см. в интерактивном руководстве по F#.
Интерполяция строк
Интерполированные строки F# довольно похожи на интерполированные строки C# или JavaScript, что позволяет писать код в "отверстия" внутри строкового литерала. Простой пример:
let name = "Phillip"
let age = 29
printfn $"Name: {name}, Age: {age}"
printfn $"I think {3.0 + 0.14} is close to {System.Math.PI}!"
Однако интерполированные строки F# также позволяют выполнять типизированные интерполяции так же, как sprintf
и функция, для принудительного применения выражения внутри интерполированного контекста, соответствующего конкретному типу. В нем используются те же описатели формата.
let name = "Phillip"
let age = 29
printfn $"Name: %s{name}, Age: %d{age}"
// Error: type mismatch
printfn $"Name: %s{age}, Age: %d{name}"
В предыдущем типизированном примере %s
интерполяции требуется интерполяция типа string
, в то время как %d
для интерполяции требуется integer
интерполяция.
Кроме того, любое произвольное выражение F# (или выражения) можно поместить в сторону контекста интерполяции. Даже можно написать более сложное выражение, например:
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]
}
"""
Хотя мы не рекомендуем делать это слишком много на практике.
Эта функция реализует F# RFC FS-1001.
Поддержка nameof
F# 5 поддерживает nameof
оператор, который разрешает символ, который он используется для и создает его имя в источнике F#. Это полезно в различных сценариях, таких как ведение журнала и защита журнала от изменений в исходном коде.
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}"
Последняя строка вызовет исключение и "месяц" будет отображаться в сообщении об ошибке.
Вы можете назвать почти каждую конструкцию F#:
module M =
let f x = nameof x
printfn $"{M.f 12}"
printfn $"{nameof M}"
printfn $"{nameof M.f}"
Три последних дополнения — это изменение того, как работают операторы: добавление формы для параметров универсального nameof<'type-parameter>
типа и возможность использования nameof
в качестве шаблона в выражении сопоставления шаблонов.
При выборе имени оператора предоставляется исходная строка. Если вам нужна скомпилированная форма, используйте скомпилированное имя оператора:
nameof(+) // "+"
nameof op_Addition // "op_Addition"
Для использования имени параметра типа требуется немного другой синтаксис:
type C<'TType> =
member _.TypeName = nameof<'TType>
Это похоже на typeof<'T>
операторы и typedefof<'T>
операторы.
F# 5 также добавляет поддержку nameof
шаблона, который можно использовать в 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
Предыдущий код использует "nameof" вместо строкового литерала в выражении сопоставления.
Эта функция реализует F# RFC FS-1003.
Объявления открытых типов
F# 5 также добавляет поддержку объявлений открытых типов. Объявление открытого типа похоже на открытие статического класса в C#, за исключением некоторого другого синтаксиса и немного другого поведения в соответствии с семантикой F#.
С помощью объявлений открытых типов можно open
использовать любой тип для предоставления статического содержимого внутри него. Кроме того, для предоставления их содержимого можно использовать open
профсоюзы и записи, определяемые F#. Например, это может быть полезно, если у вас есть объединение, определенное в модуле, и хотите получить доступ к его случаям, но не хотите открывать весь модуль.
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}"
В отличие от C#, при open type
использовании двух типов, которые предоставляют член с одинаковым именем, член из последнего типа open
теняет другое имя. Это согласуется с семантикой F# вокруг теневого тенирования, которые уже существуют.
Эта функция реализует F# RFC FS-1068.
Согласованное поведение срезов для встроенных типов данных
Поведение для срезов встроенных FSharp.Core
типов данных (массив, список, строка, 2D-массив, трехмерный массив, 4D-массив) используется не для согласованности до F# 5. Некоторые граничные случаи поведения вызвали исключение, и некоторые не будут. В F# 5 все встроенные типы теперь возвращают пустые срезы для срезов, которые невозможно создать:
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)]
Эта функция реализует F# RFC FS-1077.
Срезы фиксированного индекса для трехмерных и 4D-массивов в FSharp.Core
F# 5 обеспечивает поддержку срезов с фиксированным индексом в встроенных трехмерных и 4D-типах массивов.
Чтобы проиллюстрировать это, рассмотрим следующий трехмерный массив:
z = 0
x\y | 0 | 1 |
---|---|---|
0 | 0 | 1 |
1 | 2 | 3 |
z = 1
x\y | 0 | 1 |
---|---|---|
0 | 4 | 5 |
1 | 6 | 7 |
Что делать, если вы хотите извлечь срез [| 4; 5 |]
из массива? Теперь это очень просто!
// 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]
Эта функция реализует F# RFC FS-1077b.
Улучшения кавычки F#
В кавычках кода F# теперь есть возможность хранить сведения о ограничении типа. Рассмотрим следующий пример:
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
Ограничение, inline
созданное функцией, сохраняется в кавычках кода. Теперь negate
можно оценить кавычки функции.
Эта функция реализует F# RFC FS-1071.
Применимые выражения вычислений
Выражения вычислений (CES) используются сегодня для моделирования "контекстных вычислений" или в более функциональной терминологии, монадических вычислений.
F# 5 представляет применимые CES, которые предлагают другую вычислительную модель. Применимые ЦС позволяют более эффективным вычислениям, если каждое вычисление является независимым, и их результаты накапливаются в конце. Если вычисления не зависят друг от друга, они также тривиально параллелизуются, позволяя авторам CE писать более эффективные библиотеки. Это преимущество приходится на ограничение, однако: вычисления, зависящие от ранее вычисляемых значений, не допускаются.
В следующем примере показан базовый применимый CE для 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
Если вы являетесь автором библиотеки, который предоставляет CES в своей библиотеке сегодня, вам потребуется учитывать некоторые дополнительные рекомендации.
Эта функция реализует F# RFC FS-1063.
Интерфейсы можно реализовать в разных универсальных экземплярах
Теперь вы можете реализовать один и тот же интерфейс в разных универсальных экземплярах:
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"
Эта функция реализует F# RFC FS-1031.
Использование элементов интерфейса по умолчанию
F# 5 позволяет использовать интерфейсы с реализацией по умолчанию.
Рассмотрим интерфейс, определенный в C# следующим образом:
using System;
namespace CSharp
{
public interface MyDim
{
public int Z => 0;
}
}
Его можно использовать в F# с помощью любого из стандартных средств реализации интерфейса:
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}"
Это позволяет безопасно использовать преимущества кода C# и компонентов .NET, написанных на современном языке C#, когда они ожидают, что пользователи смогут использовать реализацию по умолчанию.
Эта функция реализует F# RFC FS-1074.
Упрощенное взаимодействие с типами значений, допускающих значение NULL
Типы, допускающие значение NULL (называемые типы , допускающие значение null исторически), уже давно поддерживаются F#, но взаимодействие с ними традиционно было несколько болью, так как вам придется создавать Nullable
или Nullable<SomeType>
оболочку каждый раз, когда вы хотите передать значение. Теперь компилятор неявно преобразует тип значения в Nullable<ThatValueType>
тип, если целевой тип совпадает. Теперь возможен следующий код:
#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")))
Эта функция реализует F# RFC FS-1075.
Предварительная версия: обратные индексы
F# 5 также представляет предварительную версию для разрешения обратных индексов. Синтаксис: ^idx
. Вот как можно использовать значение элемента 1 из конца списка:
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
Можно также определить обратные индексы для собственных типов. Для этого необходимо реализовать следующий метод:
GetReverseIndex: dimension: int -> offset: int
Ниже приведен пример для 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
Эта функция реализует F# RFC FS-1076.
Предварительная версия: перегрузки пользовательских ключевое слово в выражениях вычислений
Выражения вычислений — это мощная функция для авторов библиотеки и платформы. Они позволяют значительно улучшить экспрессивность компонентов, позволяя определять известные члены и формировать DSL для домена, в который вы работаете.
F# 5 добавляет поддержку предварительной версии для перегрузки пользовательских операций в выражениях вычислений. Он позволяет записывать и использовать следующий код:
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)
}
До этого изменения можно написать InputBuilder
тип как это так, но его нельзя использовать в примере. Так как перегрузки, необязательные параметры и теперь System.ParamArray
типы разрешены, все работает так же, как и ожидалось.
Эта функция реализует F# RFC FS-1056.