Поделиться через


Новые возможности F# 6

F# 6 добавляет несколько улучшений в язык F# и F# Interactive. Она выпущена с .NET 6.

Скачать последний пакет SDK для .NET можно на странице загрузки .NET.

Начало работы

F# 6 доступен во всех дистрибутивах .NET Core и инструментах Visual Studio. Дополнительные сведения см. в статье "Начало работы с F#".

задача {...}

F# 6 включает встроенную поддержку разработки задач .NET в коде F# с помощью выражений задач. Выражения задач похожи на асинхронные выражения, но позволяют создавать задачи .NET напрямую.

Например, рассмотрим следующий код F# для создания задачи, совместимой с .NET.

let readFilesTask (path1, path2) =
   async {
        let! bytes1 = File.ReadAllBytesAsync(path1) |> Async.AwaitTask
        let! bytes2 = File.ReadAllBytesAsync(path2) |> Async.AwaitTask
        return Array.append bytes1 bytes2
   } |> Async.StartAsTask

С помощью F# 6 этот код можно переписать следующим образом.

let readFilesTask (path1, path2) =
   task {
        let! bytes1 = File.ReadAllBytesAsync(path1)
        let! bytes2 = File.ReadAllBytesAsync(path2)
        return Array.append bytes1 bytes2
   }

Поддержка задач была доступна для F# 5 с помощью отличных библиотек TaskBuilder.fs и Ply. Необходимо просто перенести код в встроенную поддержку. Однако существуют некоторые различия: пространства имен и вывод типов немного отличаются между встроенной поддержкой и этими библиотеками, и могут понадобиться некоторые дополнительные аннотации типов. При необходимости эти библиотеки сообщества по-прежнему можно использовать с F# 6, если вы ссылаетесь на них явно и открываете правильные пространства имен в каждом файле.

Использование task {…} очень похоже на использование async {…}. Использование task {…} имеет несколько преимуществ по сравнению async {…}с:

  • Затраты на выполнение task {...} ниже, что, возможно, улучшает производительность в активно используемых участках кода, где асинхронная работа выполняется быстро.
  • Улучшены отладочные шаги и трассировки стека для task {…}.
  • Взаимодействие с пакетами .NET, которые ожидают или создают задачи, проще.

Если вы знакомы с async {…}, есть некоторые отличия, которые следует учитывать:

  • task {…} немедленно выполняет задачу до первой точки ожидания.
  • task {…} не распространяет маркер отмены неявно.
  • task {…} не выполняет неявных проверок отмены.
  • task {…} не поддерживает асинхронные хвостовые вызовы. Это означает, что использование return! .. рекурсивно может привести к переполнению стека, если нет промежуточных асинхронных ожиданий.

Как правило, следует рассмотреть возможность использования task {…} вместо async {…} в новом коде, если вы работаете с библиотеками .NET, которые используют задачи, и если вы не полагаетесь на асинхронные вызовы хвостовых функций или неявную передачу маркера отмены. В существующем коде следует переключаться на task {…} только после того, как вы проверите свой код, чтобы убедиться в том, что вы не полагаетесь на ранее упомянутые характеристики async {…}.

Эта функция реализует F# RFC FS-1097.

Более простой синтаксис индексирования с помощью expr[idx]

F# 6 позволяет использовать синтаксис expr[idx] для индексирования и срезов коллекций.

До и включая F# 5, F# использовал expr.[idx] в качестве синтаксиса индексирования. Разрешение на использование expr[idx] основано на повторяющихся отзывах от тех, кто изучает F# или впервые сталкивается с F#, что использование точечной нотации рассматривается как ненужное отклонение от стандартной отраслевой практики.

Это не критическое изменение, так как по умолчанию предупреждения не создаются при использовании expr.[idx]. Однако некоторые информационные сообщения, предлагающие уточнения кода, выдаются. Кроме того, можно активировать дополнительные информационные сообщения. Например, можно активировать необязательное информационное предупреждение (/warnon:3366), чтобы начать сообщать об использовании нотации expr.[idx]. Дополнительные сведения см. в нотации индексатора.

В новом коде рекомендуется систематическое использование expr[idx] в качестве синтаксиса индексирования.

Эта функция реализует F# RFC FS-1110.

Представления структуры для частичных активных шаблонов

F# 6 расширяет функцию "активные шаблоны" с необязательными представлениями структуры для частичных активных шаблонов. Это позволяет использовать атрибут для лимитации частично активного шаблона с целью возвращения опции значения.

[<return: Struct>]
let (|Int|_|) str =
   match System.Int32.TryParse(str) with
   | true, int -> ValueSome(int)
   | _ -> ValueNone

Требуется использование атрибута. На сайтах использования код не изменяется. Чистый результат заключается в том, что выделение уменьшается.

Эта функция реализует F# RFC FS-1039.

Перегруженные пользовательские операции в выражениях вычислений

F# 6 позволяет использовать CustomOperationAttribute в перегруженных методах.

Рассмотрим следующее использование построителя contentвыражений вычислений:

let mem = new System.IO.MemoryStream("Stream"B)
let content = ContentBuilder()
let ceResult =
    content {
        body "Name"
        body (ArraySegment<_>("Email"B, 0, 5))
        body "Password"B 2 4
        body "BYTES"B
        body mem
        body "Description" "of" "content"
    }

body Здесь пользовательская операция принимает разное количество аргументов разных типов. Это поддерживается реализацией следующего построителя, использующего перегрузку:

type Content = ArraySegment<byte> list

type ContentBuilder() =
    member _.Run(c: Content) =
        let crlf = "\r\n"B
        [|for part in List.rev c do
            yield! part.Array[part.Offset..(part.Count+part.Offset-1)]
            yield! crlf |]

    member _.Yield(_) = []

    [<CustomOperation("body")>]
    member _.Body(c: Content, segment: ArraySegment<byte>) =
        segment::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, bytes: byte[]) =
        ArraySegment<byte>(bytes, 0, bytes.Length)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, bytes: byte[], offset, count) =
        ArraySegment<byte>(bytes, offset, count)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, content: System.IO.Stream) =
        let mem = new System.IO.MemoryStream()
        content.CopyTo(mem)
        let bytes = mem.ToArray()
        ArraySegment<byte>(bytes, 0, bytes.Length)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, [<ParamArray>] contents: string[]) =
        List.rev [for c in contents -> let b = Text.Encoding.ASCII.GetBytes c in ArraySegment<_>(b,0,b.Length)] @ c

Эта функция реализует F# RFC FS-1056.

Шаблоны "как"

В F# 6 правая сторона as шаблона теперь сама может быть шаблоном. Это важно, если тест типа дал более строгий тип входным данным. Например, рассмотрим следующий код:

type Pair = Pair of int * int

let analyzeObject (input: obj) =
    match input with
    | :? (int * int) as (x, y) -> printfn $"A tuple: {x}, {y}"
    | :? Pair as Pair (x, y) -> printfn $"A DU: {x}, {y}"
    | _ -> printfn "Nope"

let input = box (1, 2)

Во всех случаях входной объект проверяется на тип. Правая сторона as шаблона теперь разрешена быть дополнительным шаблоном, который сам по себе может соответствовать объекту с более строгим типом.

Эта функция реализует F# RFC FS-1105.

Правки синтаксиса отступов

F# 6 удаляет ряд несоответствий и ограничений при использовании синтаксиса, чувствительного к отступам. См. RFC FS-1108. Это устраняет 10 существенных проблем, выделенных пользователями F# с версии 4.0.

Например, в F# 5 разрешен следующий код:

let c = (
    printfn "aaaa"
    printfn "bbbb"
)

Однако следующий код не разрешен (он создал предупреждение):

let c = [
    1
    2
]

В F# 6 оба разрешены. Это делает F# более простым и легким для изучения. Участник сообщества F# Хадриан Тан возглавил эту работу, включая замечательное и весьма ценное систематическое тестирование функции.

Эта функция реализует F# RFC FS-1108.

Дополнительные неявные преобразования

В F# 6 мы активировали поддержку дополнительных "имплицитных" и "управляемых типом" преобразований, как описано в RFC FS-1093.

Это изменение дает три преимущества:

  1. Требуется меньше явных переадресов
  2. Требуется меньше явных целочисленных преобразований
  3. Добавлена полноценная поддержка неявных преобразований в стиле .NET.

Эта функция реализует F# RFC FS-1093.

Дополнительные неявные преобразования к базовому типу

F# 6 реализует дополнительные неявные преобразования передачи. Например, в F# 5 и более ранних версиях были необходимы восходящие преобразования для выражения возврата при реализации функции, в которой выражения имели разные подтипы в разных ветвях, даже при наличии аннотации типа. Рассмотрим следующий код F# 5:

open System
open System.IO

let findInputSource () : TextReader =
    if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
        // On Monday a TextReader
        Console.In
    else
        // On other days a StreamReader
        File.OpenText("path.txt") :> TextReader

Здесь ветви условного вычисления вычисляют TextReader и StreamReader соответственно, а поднятие типа добавлено для того, чтобы обе ветви имели тип StreamReader. В F# 6 эти upcasts теперь добавляются автоматически. Это означает, что код проще:

let findInputSource () : TextReader =
    if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
        // On Monday a TextReader
        Console.In
    else
        // On other days a StreamReader
        File.OpenText("path.txt")

По желанию можно включить предупреждение /warnon:3388, чтобы показывать предупреждение в каждой точке, где используется дополнительное неявное расширение, как описано в дополнительных предупреждениях для неявных преобразований.

Неявные преобразования целых чисел

В F# 6 32-разрядные целые числа расширяются до 64-разрядных целых чисел, если оба типа известны. Например, рассмотрим типичную форму API:

type Tensor(…) =
    static member Create(sizes: seq<int64>) = Tensor(…)

В F# 5 целые литералы для int64 должны использоваться:

Tensor.Create([100L; 10L; 10L])

или

Tensor.Create([int64 100; int64 10; int64 10])

В F# 6 расширение выполняется автоматически для int32 к int64, int32 к nativeint, и int32 к double, когда исходный и целевой тип известны во время вывода типов. Поэтому в таких случаях, как предыдущие примеры, int32 можно использовать литералы:

Tensor.Create([100; 10; 10])

Несмотря на это изменение, F# продолжает использовать явное расширение числовых типов в большинстве случаев. Например, неявное расширение не применяется к другим числовым типам, таким как int8 или int16, или изfloat32float64, или когда исходный или целевой тип неизвестен. Вы также можете опционально включить предупреждение /warnon:3389 для отображения сообщения в каждой точке, где используется неявное числовое расширение типа, как описано в дополнительных предупреждениях для неявных преобразований.

Высококачественная поддержка неявных преобразований в стиле .NET

В F# 6 преобразования .NET "op_Implicit" применяются автоматически в коде F# при вызове методов. Например, в F# 5 необходимо было использовать XName.op_Implicit при работе с API .NET для XML:

open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants(XName.op_Implicit "Item")

В F# 6 op_Implicit преобразования автоматически применяются к выражениям аргументов, когда доступны типы исходного и целевого выражения.

open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants("Item")

При необходимости можно включить предупреждение /warnon:3395, чтобы показать предупреждение во всех случаях op_Implicit, где аргументам метода используются расширяющие преобразования, как описано в Необязательные предупреждения для неявных преобразований.

Замечание

В первом выпуске F# 6 этот номер предупреждения был /warnon:3390. Из-за конфликта номер предупреждения был обновлен до /warnon:3395.

Опциональные предупреждения для неявных преобразований

Типо-ориентированные и неявные преобразования могут плохо сочетаться с выводом типов и усложнить понимание кода. По этой причине некоторые средства устранения рисков существуют для обеспечения того, чтобы эта функция не злоупотреблялась в коде F#. Во-первых, как исходный, так и конечный тип должны быть строго известны, без неоднозначности или дополнительного вывода типов. Во-вторых, предупреждения о согласии можно активировать, чтобы сообщить об использовании неявных преобразований с одним предупреждением по умолчанию:

  • /warnon:3388 (дополнительная неявная рассылка)
  • /warnon:3389 (неявное расширение числовых элементов)
  • /warnon:3391 (op_Implicit в неметодических аргументах, по умолчанию)
  • /warnon:3395 (op_Implicit в аргументах метода)

Если ваша команда хочет запретить все использование неявных преобразований, можно также указать /warnaserror:3388, /warnaserror:3389/warnaserror:3391и /warnaserror:3395.

Форматирование двоичных чисел

F# 6 добавляет %B шаблон в доступные описатели формата для форматов двоичных чисел. Рассмотрим следующий код F#:

printf "%o" 123
printf "%B" 123

Этот код выводит следующие выходные данные:

173
1111011

Эта функция реализует F# RFC FS-1100.

Отменяет использование привязок

F# 6 позволяет _ использовать в привязке use , например:

let doSomething () =
    use _ = System.IO.File.OpenText("input.txt")
    printfn "reading the file"

Эта функция реализует F# RFC FS-1102.

InlineIfLambda

Компилятор F# включает оптимизатор, выполняющий встраивание кода. В F# 6 мы добавили новую декларативную возможность, которая позволяет коду при необходимости указывать, что, если аргумент определяется как лямбда-функция, этот аргумент должен всегда встраиваться в местах вызова.

Например, рассмотрим следующую iterateTwice функцию для обхода массива:

let inline iterateTwice ([<InlineIfLambda>] action) (array: 'T[]) =
    for j = 0 to array.Length-1 do
        action array[j]
    for j = 0 to array.Length-1 do
        action array[j]

Если место вызова:

let arr = [| 1.. 100 |]
let mutable sum = 0
arr  |> iterateTwice (fun x ->
    sum <- sum + x)

Затем после встраивания и других оптимизаций код становится следующим:

let arr = [| 1.. 100 |]
let mutable sum = 0
for j = 0 to arr.Length-1 do
    sum <- sum + arr[j]
for j = 0 to arr.Length-1 do
    sum <- sum + arr[j]

В отличие от предыдущих версий F#, эта оптимизация применяется независимо от размера лямбда-выражения. Эту функцию также можно использовать для реализации развёртки циклов и аналогичных преобразований более надёжным образом.

Предупреждение о согласии (/warnon:3517отключено по умолчанию) можно включить, чтобы указать места в коде, где InlineIfLambda аргументы не привязаны к лямбда-выражениям на сайтах вызовов. В обычных ситуациях это предупреждение не должно быть включено. Однако в некоторых типах высокопроизводительного программирования может быть полезно убедиться, что весь код встраивается и уплощён.

Эта функция реализует F# RFC FS-1098.

Возобновляемый код

Поддержка task {…} F# 6 основана на основе повторного кодаRFC FS-1087. Возобновляемый код — это техническая функция, которая может использоваться для создания множества высокопроизводительных асинхронных и приостанавливаемых состояний машин.

Дополнительные функции коллекции

FSharp.Core 6.0.0 добавляет пять новых операций в основные функции коллекции. Ниже приведены следующие функции:

  • List/Array/Seq.insertAt
  • List/Array/Seq.removeAt
  • Список/Массив/Последовательность.updateAt
  • List/Array/Seq.insertManyAt
  • List/Array/Seq.removeManyAt

Эти функции выполняют операции копирования и обновления для соответствующего типа коллекции или последовательности. Этот тип операции представляет собой форму функционального обновления. Примеры использования этих функций см. в соответствующей документации, например List.insertAt.

Например, рассмотрим модель, сообщение и логику обновления для простого приложения Todo List, написанного в стиле Elmish. Здесь пользователь взаимодействует с приложением, создает сообщения и update обрабатывает эти сообщения, создавая новую модель:

type Model =
    { ToDo: string list }

type Message =
    | InsertToDo of index: int * what: string
    | RemoveToDo of index: int
    | LoadedToDos of index: int * what: string list

let update (model: Model) (message: Message) =
    match message with
    | InsertToDo (index, what) ->
        { model with ToDo = model.ToDo |> List.insertAt index what }
    | RemoveToDo index ->
        { model with ToDo = model.ToDo |> List.removeAt index }
    | LoadedToDos (index, what) ->
        { model with ToDo = model.ToDo |> List.insertManyAt index what }

С этими новыми функциями логика понятна и проста и зависит только от неизменяемых данных.

Эта функция реализует F# RFC FS-1113.

Карта содержит ключи и значения

В FSharp.Core 6.0.0 тип Map теперь поддерживает свойства "Ключи и значения ". Эти свойства не копируют базовую коллекцию.

Эта функция описана в F# RFC FS-1113.

Дополнительные встроенные компоненты для NativePtr

FSharp.Core 6.0.0 добавляет новые встроенные компоненты в модуль NativePtr :

  • NativePtr.nullPtr
  • NativePtr.isNullPtr
  • NativePtr.initBlock
  • NativePtr.clear
  • NativePtr.copy
  • NativePtr.copyBlock
  • NativePtr.ofILSigPtr
  • NativePtr.toILSigPtr

Как и в других функциях, эти функции в NativePtr встраиваются, и их использование выдает предупреждения, если /nowarn:9 не используется. Использование этих функций ограничивается неуправляемыми типами.

Эта функция описана в F# RFC FS-1109.

Дополнительные числовые типы с аннотациями единиц

В F# 6 следующие типы или псевдонимы сокращенного типа теперь поддерживают аннотации единиц измерения. Новые дополнения отображаются полужирным шрифтом:

F# псевдоним Тип CLR
float32/single System.Single
float/double System.Double
decimal System.Decimal
sbyte/int8 System.SByte
int16 System.Int16
int/int32 System.Int32
int64 System.Int64
byte/uint8 System.Byte
uint16 System.UInt16
uint/uint32 System.UInt32
uint64 System.UIn64
nativeint System.IntPtr
unativeint System.UIntPtr

Например, можно аннотировать целое число без знака следующим образом:

[<Measure>]
type days

let better_age = 3u<days>

Эта функция описана в F# RFC FS-1091.

Информационные предупреждения для редко используемых символьных операторов

F# 6 добавляет мягкое руководство, которое денормализует использование :=, !, incr и decr в F# 6 и более. С использованием этих операторов и функций создаются информационные сообщения, которые предлагают заменить код явным использованием свойства Value.

В программировании на F# ссылочные ячейки можно использовать для изменяемых регистров, размещённых в куче. Хотя они иногда полезны, в современном кодировании на F# они редко используются, потому что вместо этого можно использовать let mutable. Базовая библиотека F# включает два оператора := и ! две функции incr , а decr также связанные с вызовами ссылок. Наличие этих операторов делает ссылочные ячейки более центральными для программирования F#, чем они должны быть, требуя, чтобы все программисты F# знали эти операторы. Кроме того, ! оператор можно легко путать с not операцией на C# и других языках, потенциально тонким источником ошибок при переводе кода.

Обоснование этого изменения заключается в сокращении числа операторов, которые программист F# должен знать, и таким образом упростить F# для начинающих.

Например, рассмотрим следующий код F# 5:

let r = ref 0

let doSomething() =
    printfn "doing something"
    r := !r + 1

Во-первых, ссылочные ячейки редко требуются в современном кодировании F#, так как let mutable обычно можно использовать вместо этого:

let mutable r = 0

let doSomething() =
    printfn "doing something"
    r <- r + 1

При использовании ссылочных ячеек F# 6 выдает информационное предупреждение с просьбой изменить последнюю строку на r.Value <- r.Value + 1, и предоставляя ссылку на дополнительные рекомендации по соответствующему использованию ссылочных ячеек.

let r = ref 0

let doSomething() =
    printfn "doing something"
    r.Value <- r.Value + 1

Эти сообщения не являются предупреждениями; они представляют собой информационные сообщения, отображаемые в выходных данных интегрированной среды разработки и компилятора. F# сохраняет обратную совместимость.

Эта функция реализует F# RFC FS-1111.

Инструменты F#: .NET 6 по умолчанию для сценариев в Visual Studio

Если вы открываете или выполняете скрипт F# (.fsx) в Visual Studio, по умолчанию скрипт будет анализироваться и выполняться с помощью .NET 6 с 64-разрядным выполнением. Эта функция была в предварительной версии в более поздних выпусках Visual Studio 2019 и теперь включена по умолчанию.

Чтобы включить скрипты .NET Framework, выберите Инструменты>Параметры>Инструменты F#>F# Интерактивный. Установите для параметра "Использовать скрипты .NET Core" значение false, а затем перезапустите интерактивное окно F#. Этот параметр влияет как на редактирование скрипта, так и на выполнение скрипта. Чтобы включить 32-разрядное выполнение скриптов .NET Framework, также установите 64-разрядный интерактивный F# на значение false. Для скриптов .NET Core нет 32-разрядного варианта.

Инструментарий F#: зафиксируйте версию SDK для ваших F# скриптов

Если вы выполняете скрипт с помощью dotnet fsi каталога, содержащего файлglobal.json с параметром пакета SDK для .NET, то для выполнения и редактирования скрипта будет использоваться указанная версия пакета SDK для .NET. Эта функция доступна в более поздних версиях F# 5.

Например, предположим, что в каталоге есть скрипт со следующим global.json файлом, указывающим политику версии пакета SDK для .NET:

{
  "sdk": {
    "version": "5.0.200",
    "rollForward": "minor"
  }
}

Если вы сейчас выполните скрипт с использованием dotnet fsi, из этого каталога будет учитываться версия SDK. Это мощная функция, которая позволяет "заблокировать" пакет SDK, используемый для компиляции, анализа и выполнения скриптов.

Если вы открываете и редактируете скрипт в Visual Studio и других средах разработки, средство будет учитывать этот параметр при анализе и проверке скрипта. Если пакет SDK не найден, его необходимо установить на компьютере разработки.

В Linux и других системах Unix вы можете объединить это с шебангом , чтобы также указать языковую версию для прямого выполнения скрипта. Простой шебанг для script.fsx :

#!/usr/bin/env -S dotnet fsi

printfn "Hello, world"

Теперь скрипт можно выполнять напрямую с помощью script.fsx. Это можно объединить с определенной версией языка, отличной от по умолчанию, следующим образом:

#!/usr/bin/env -S dotnet fsi --langversion:5.0

Замечание

Этот параметр игнорируется средствами редактирования, которые будут анализировать скрипт при условии последней версии языка.

Удаление устаревших функций

С момента F# 2.0 некоторые устаревшие функции уже давно выдают предупреждения. Использование этих функций в F# 6 дает ошибки, если вы явно не используете /langversion:5.0. Ниже приведены функции, которые дают ошибки:

  • Например, несколько универсальных параметров с использованием имени (int, int) Dictionaryтипа postfix. Это становится ошибкой в F# 6. Вместо этого следует использовать стандартный синтаксис Dictionary<int,int> .
  • #indent "off". Это становится ошибкой.
  • x.(expr). Это становится ошибкой.
  • module M = struct … end . Это становится ошибкой.
  • Использование входных данных *.ml и *.mli. Это становится ошибкой.
  • Использование (*IF-CAML*) или (*IF-OCAML*). Это становится ошибкой.
  • Использование операторов land, , lor, lxorlsllsrили asr в качестве операторов infix. Это инфиксные ключевые слова в F#, поскольку они были инфиксными ключевыми словами в OCaml и не определены в FSharp.Core. Использование этих ключевых слов теперь вызывает предупреждение.

Это реализует F# RFC FS-1114.