Перейти к основному содержимому

5.04. Справочник про F#

Разработчику Архитектору

Справочник про F#

Основы синтаксиса, типы и значения

1. Общие принципы языка

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

Код в F# организован в выражения, а не в последовательности команд. Каждое выражение имеет значение и тип.

2. Базовые типы

Примитивные типы

  • bool — логический тип (true, false)
  • int — 32-битное целое число
  • int8, int16, int32, int64 — целые числа с явным указанием размера
  • uint8, uint16, uint32, uint64 — беззнаковые целые
  • float — 64-битное число с плавающей точкой (IEEE 754 double)
  • float32 — 32-битное число с плавающей точкой (IEEE 754 single)
  • decimal — десятичное число с высокой точностью для финансовых вычислений
  • char — одиночный символ Unicode
  • string — неизменяемая строка Unicode
  • unit — тип с единственным значением (), используется как «ничего не возвращается»

Составные типы

  • Кортежи (tuples) — фиксированные наборы значений разных типов
    Пример: (1, "hello", true)
    Тип: int * string * bool

  • Списки (lists) — односвязные иммутабельные списки одного типа
    Пример: [1; 2; 3]
    Тип: int list
    Пустой список: []
    Конструктор: head :: tail
    Операторы: @ — конкатенация списков

  • Массивы (arrays) — изменяемые последовательности фиксированной длины
    Пример: [|1; 2; 3|]
    Тип: int[] или int array

  • Последовательности (sequences) — ленивые потоки значений
    Пример: seq { 1 .. 10 }
    Тип: seq<int>

  • Записи (records) — именованные структуры с полями

    type Person = { Name: string; Age: int }
    let p = { Name = "Alice"; Age = 30 }
  • Размеченные объединения (discriminated unions) — типы с несколькими вариантами

    type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
    let s = Circle 5.0
  • Опции (option) — стандартный тип для представления наличия или отсутствия значения

    let x: int option = Some 42
    let y: int option = None
  • Результат (Result) — тип для обработки успешных и ошибочных результатов

    type Result<'T, 'Error> =
    | Ok of 'T
    | Error of 'Error

3. Литералы и синтаксические формы

Числовые литералы

  • Целые: 42, -17, 0x1A (шестнадцатеричные), 0b1010 (двоичные)
  • Вещественные: 3.14, 1e-5, 2.5f (суффикс f для float32)
  • Десятичные: 123.45m (суффикс m)

Строковые литералы

  • Обычные: "Hello"
  • Многострочные: """Line 1\nLine 2"""
  • Вербальные (verbatim): @"C:\path\to\file" — обратные слэши не экранируются
  • Интерполированные: $"Hello, {name}!"

Символьные литералы

  • 'a', '\n', '\t', '\u0041'

4. Имена и привязки

В F# используются привязки (let) вместо присваивания.

let x = 42
let y = x + 1

Привязки иммутабельны по умолчанию. Для создания изменяемых значений используется ключевое слово mutable:

let mutable counter = 0
counter <- counter + 1

Оператор <- используется для изменения значения mutable переменной.

5. Функции

Функции определяются через let:

let add a b = a + b

Тип выводится автоматически: val add : int -> int -> int

Функции — значения первого класса. Их можно передавать, возвращать, хранить в структурах.

Каррирование

Функция add каррирована: add 1 возвращает новую функцию int -> int.

Явное указание типов

let add (a: int) (b: int) : int = a + b

Анонимные функции

let square = fun x -> x * x
List.map (fun x -> x * 2) [1; 2; 3]

Рекурсия

Для рекурсивных функций требуется rec:

let rec factorial n =
if n <= 1 then 1 else n * factorial (n - 1)

Хвостовая рекурсия

F# оптимизирует хвостовую рекурсию в цикл. Используется аккумулятор:

let factorial n =
let rec loop acc i =
if i <= 1 then acc else loop (acc * i) (i - 1)
loop 1 n

6. Сопоставление с образцом (Pattern Matching)

Основной механизм ветвления и деструктуризации:

let describeNumber x =
match x with
| 0 -> "zero"
| 1 -> "one"
| n when n > 0 -> "positive"
| _ -> "negative or other"

Поддерживает:

  • литералы
  • переменные
  • условия (when)
  • кортежи
  • списки (head :: tail)
  • записи
  • размеченные объединения

Пример с кортежем:

let swap (a, b) = (b, a)

Пример с записью:

let greet { Name = name } = $"Hello, {name}!"

7. Операторы

F# использует следующие операторы:

  • Арифметические: +, -, *, /, %
  • Сравнения: =, <>, <, <=, >, >=
  • Логические: &&, ||, not
  • Битовые: &&&, |||, ^^^, ~~~, <<<, >>>
  • Списковые: :: (конструктор), @ (конкатенация)
  • Присваивания: <- (только для mutable)
  • Типизации: : (аннотация типа), :> (преобразование к базовому типу), :?> (динамическое приведение)

Операторы можно определять как функции:

let (+.) a b = sqrt(a ** 2.0 + b ** 2.0)

8. Комментарии

  • Однострочные: // комментарий
  • Многострочные: (* комментарий *)

9. Модульность

Код организуется в модули и пространства имён.

namespace MyNamespace

module MathUtils =
let pi = 3.14159
let circleArea r = pi * r * r

Импорт других модулей:

open System
open List

10. Типобезопасность и вывод типов

F# использует алгоритм Хиндли-Милнера для вывода типов. Большинство типов не нужно указывать явно.

Компилятор гарантирует:

  • Отсутствие null-ссылок (кроме взаимодействия с .NET)
  • Безопасность шаблонов (все случаи размеченного объединения должны быть обработаны)
  • Иммутабельность по умолчанию

Коллекции, последовательности, LINQ-подобные операции, асинхронность и взаимодействие с .NET

1. Коллекции и стандартные модули

F# предоставляет богатый набор функций для работы с коллекциями через модули List, Array, Seq, Map, Set.

Модуль List

Основные функции:

  • List.map f xs — преобразует каждый элемент списка
  • List.filter p xs — оставляет элементы, удовлетворяющие предикату
  • List.fold f acc xs — сворачивает список слева направо
  • List.reduce f xs — сворачивает непустой список без начального аккумулятора
  • List.iter f xs — применяет действие к каждому элементу (для побочных эффектов)
  • List.length xs — возвращает длину списка
  • List.isEmpty xs — проверяет, пуст ли список
  • List.rev xs — разворачивает список
  • List.append xs ys — объединяет два списка (xs @ ys)
  • List.concat xss — объединяет список списков в один список
  • List.exists p xs — проверяет наличие элемента, удовлетворяющего условию
  • List.forall p xs — проверяет, что все элементы удовлетворяют условию
  • List.tryFind p xs — возвращает Some x, если найден элемент, иначе None
  • List.partition p xs — разделяет список на два: удовлетворяющие и неудовлетворяющие предикату
  • List.zip xs ys — объединяет два списка в список пар
  • List.unzip pairs — разделяет список пар на два списка

Модуль Array

Аналогичен List, но работает с изменяемыми массивами:

  • Array.map, Array.filter, Array.iter, Array.length и т.д.
  • Array.create n v — создаёт массив длины n, заполненный значением v
  • Array.zeroCreate n — создаёт массив с нулевыми значениями (для числовых типов) или null (для ссылочных типов)
  • Array.set arr i v — устанавливает значение по индексу
  • Array.get arr i — получает значение по индексу

Модуль Seq (последовательности)

Ленивые вычисления. Подходят для бесконечных потоков или больших данных.

  • Seq.init n f — создаёт последовательность из n элементов, генерируемых функцией
  • Seq.unfold f state — генерирует последовательность из состояния (аналог while)
  • Seq.map, Seq.filter, Seq.iter — аналогично спискам, но лениво
  • Seq.cache s — кэширует результаты для повторного использования
  • Seq.toArray, Seq.toList — материализуют последовательность

Пример бесконечной последовательности:

let naturals = Seq.initInfinite id  // 0, 1, 2, 3, ...
let evens = Seq.filter (fun x -> x % 2 = 0) naturals
let first10Evens = evens |> Seq.take 10 |> Seq.toList

2. Композиция и конвейеры

F# активно использует оператор конвейера |>:

[1..10]
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x * x)
|> List.sum

Эквивалентно:

List.sum (List.map (fun x -> x * x) (List.filter (fun x -> x % 2 = 0) [1..10]))

Также существует обратный конвейер <|:

printfn "%d" <| List.sum [1; 2; 3]

Полезен, чтобы избежать скобок при вызове функции с одним аргументом.

3. Словари и множества

Map<'Key, 'Value>

Иммутабельный ассоциативный массив на основе сбалансированного дерева.

let phoneBook = Map [("Alice", "123"); ("Bob", "456")]
let aliceNumber = Map.find "Alice" phoneBook
let updated = Map.add "Charlie" "789" phoneBook

Функции: Map.add, Map.remove, Map.containsKey, Map.find, Map.tryFind, Map.change, Map.map, Map.fold.

Set<'T>

Иммутабельное множество.

let primes = Set [2; 3; 5; 7]
let hasThree = Set.contains 3 primes
let morePrimes = Set.add 11 primes

Функции: Set.add, Set.remove, Set.contains, Set.union, Set.intersect, Set.difference.

Оба типа требуют, чтобы ключи/элементы реализовывали сравнение (comparison constraint).

4. Работа с .NET

F# полностью совместим с .NET. Можно использовать любые классы, интерфейсы, события.

Создание объектов

let list = new System.Collections.Generic.List<int>()
list.Add(42)

Или без new:

let dict = System.Collections.Generic.Dictionary<string, int>()
dict.["age"] <- 30

Свойства и индексаторы

let sb = System.Text.StringBuilder()
sb.Append("Hello") |> ignore
let text = sb.ToString()

Индексаторы используются как obj.[key].

Исключения

try
riskyOperation()
with
| :? System.IO.FileNotFoundException as ex ->
printfn "File not found: %s" ex.Message
| ex ->
printfn "Error: %s" ex.Message

Выброс исключения:

failwith "Something went wrong"
invalidArg "paramName" "Invalid argument"

5. Асинхронность

F# имеет встроенную поддержку асинхронных вычислений через асинхронные рабочие процессы (async).

Базовый синтаксис

let fetchUrlAsync url =
async {
let webClient = new System.Net.WebClient()
let! html = webClient.AsyncDownloadString(System.Uri(url))
return html.Length
}

Запуск:

let result = fetchUrlAsync "https://example.com" |> Async.RunSynchronously

Или параллельно:

let urls = ["https://a.com"; "https://b.com"]
let tasks = List.map fetchUrlAsync urls
let results = Async.Parallel tasks |> Async.RunSynchronously

Ключевые конструкции:

  • let! — ожидает завершения асинхронной операции
  • do! — выполняет асинхронное действие без привязки результата
  • return — завершает асинхронный блок с результатом
  • use / use! — автоматически освобождает ресурс после выхода из блока

Преобразование из Task (.NET)

Для работы с Task<T> из C# библиотек:

open System.Threading.Tasks

let taskToAsync (task: Task<'T>) : Async<'T> = Async.AwaitTask task

Или напрямую:

let! result = someCSharpMethodReturningTask() |> Async.AwaitTask

6. Обработка ошибок без исключений

F# поощряет использование Option и Result вместо исключений.

Пример с Result:

let divide a b =
if b = 0 then Error "Division by zero" else Ok (a / b)

match divide 10 2 with
| Ok value -> printfn "Result: %d" value
| Error msg -> printfn "Error: %s" msg

Цепочка операций:

let safeDivideChain =
divide 100 10
|> Result.bind (divide 10)
|> Result.bind (divide 2)

Модуль Result содержит: map, bind, flatMap, either, isOk, isError.

7. Интерполяция и форматирование строк

F# поддерживает интерполированные строки:

let name = "Alice"
let age = 30
printfn $"Hello, {name}! You are {age} years old."

Также можно указывать форматы:

let pi = 3.14159
printfn $"Pi ≈ {pi:F2}" // Pi ≈ 3.14

Альтернатива — sprintf для создания строки:

let message = sprintf "Value: %d" 42

8. Единицы измерения (Units of Measure)

F# позволяет прикреплять физические единицы к числам для статической проверки:

[<Measure>] type m
[<Measure>] type s

let distance = 10<m>
let time = 2<s>
let speed = distance / time // тип: float<m/s>

Эта функция работает только с float, float32, decimal.

9. Типовые ограничения и обобщения

F# поддерживает параметрический полиморфизм:

let identity x = x  // тип: 'a -> 'a
let swap (a, b) = (b, a) // тип: 'a * 'b -> 'b * 'a

Ограничения:

  • 'T : equality — тип должен поддерживать =
  • 'T : comparison — тип должен поддерживать <
  • 'T : null — тип может быть null
  • 'T : not struct — ссылочный тип
  • 'T : struct — тип значения

Пример:

let inline add a b = a + b  // работает благодаря SRTP (statically resolved type parameters)

10. Взаимодействие с другими языками .NET

F# компилируется в тот же IL-код, что и C#. Библиотеки на F# можно использовать из C#, и наоборот.

Чтобы сделать F# тип удобным для C#:

  • Использовать [<CLIMutable>] для записей (разрешает мутацию через свойства)
  • Использовать [<RequireQualifiedAccess>] для модулей, чтобы избежать конфликтов имён
  • Использовать [<AutoOpen>] для автоматического открытия модуля

Пример:

[<CLIMutable>]
type Person = { Name: string; Age: int }

Теперь в C# можно писать:

var p = new Person { Name = "Alice", Age = 30 };

Модули, пространства имён, компиляция, настройки проекта, инструменты и продвинутые возможности языка

1. Организация кода: пространства имён и модули

F# использует два основных механизма для группировки кода:

  • Пространства имён (namespace) — лёгкие контейнеры без значений, только типы и модули.
  • Модули (module) — могут содержать значения, функции, типы и вложенные модули.

Пространство имён

namespace MyLibrary

type Point = { X: float; Y: float }

module Geometry =
let distance p1 p2 =
sqrt ((p1.X - p2.X) ** 2.0 + (p1.Y - p2.Y) ** 2.0)

Файл с namespace не может содержать код верхнего уровня вне типа или модуля.

Модуль верхнего уровня

module MathUtils

let pi = 3.14159
let square x = x * x

Эквивалентно:

namespace MyProject
module MathUtils = ...

Вложенные модули

module Outer =
module Inner =
let value = 42
let getValue () = Inner.value

Использование извне:

open Outer
let x = Inner.value

Квалифицированный доступ

Если модуль помечен атрибутом [<RequireQualifiedAccess>], его функции можно вызывать только с указанием имени модуля:

[<RequireQualifiedAccess>]
module List =
let customMap f xs = ... // не конфликтует со стандартным List.map

Вызов: List.customMap f xs, а не просто customMap f xs.

Автоматическое открытие

Атрибут [<AutoOpen>] добавляет содержимое модуля в область видимости при открытии родительского пространства имён:

[<AutoOpen>]
module Helpers =
let inline (!!) x = not x

После open MyNamespace оператор !! становится доступен.


2. Файловая структура и порядок компиляции

F# чувствителен к порядку файлов в проекте. Каждый файл может ссылаться только на предыдущие файлы.

Файлы перечисляются в .fsproj в порядке компиляции:

<ItemGroup>
<Compile Include="Types.fs" />
<Compile Include="Core.fs" />
<Compile Include="Api.fs" />
</ItemGroup>

В Types.fs определяются базовые типы, в Core.fs — логика, в Api.fs — интерфейсы.

Нарушение порядка вызывает ошибку компиляции: «The value/namespace 'X' is not defined».


3. Настройки проекта (.fsproj)

Проект F# описывается в файле .fsproj (MSBuild). Основные параметры:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType> <!-- или Library -->
<WarnOn>3390;$(WarnOn)</WarnOn> <!-- дополнительные предупреждения -->
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>FS0020</NoWarn> <!-- подавление конкретного предупреждения -->
</PropertyGroup>

<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FSharp.Core" Version="8.0.401" />
</ItemGroup>
</Project>

Важные свойства

  • TargetFramework — целевая версия .NET (net6.0, net8.0, netstandard2.1)
  • OutputTypeExe (консольное приложение), Library (библиотека), WinExe (GUI без консоли)
  • OtherFlags — передача флагов компилятору F#:
    <OtherFlags>--warnon:1182 --optimize+</OtherFlags>

Компиляторные флаги F#

Через OtherFlags можно задать:

  • --optimize+ — включить оптимизации
  • --debug+ — включить отладочную информацию
  • --tailcalls+ — включить оптимизацию хвостовых вызовов (по умолчанию включена в Release)
  • --checked+ — включить проверку переполнения (по умолчанию выключена)
  • --warnon:1182 — включить предупреждение об неиспользуемых переменных
  • --nowarn:FS0020 — отключить предупреждение FS0020

4. Инструменты разработки

.NET CLI

Создание проекта:

dotnet new console -lang F# -n MyProject
dotnet build
dotnet run

Visual Studio / Visual Studio Code

  • Visual Studio — полная поддержка F# с IntelliSense, отладчиком, рефакторингом.
  • VS Code + Ionide — лёгкая среда с поддержкой F# через расширение Ionide-fsharp.

Ionide предоставляет:

  • Подсветку синтаксиса
  • Переход к определению
  • Просмотр типов
  • Запуск REPL (F# Interactive)

F# Interactive (FSI)

Интерактивный REPL запускается командой:

dotnet fsi

Внутри можно выполнять выражения:

> let x = 42;;
val x : int = 42

> [1..5] |> List.map ((*) 2);;
val it : int list = [2; 4; 6; 8; 10]

Загрузка файла:

#load "MyModule.fs";;
#load "Script.fsx";;

Скрипты используют расширение .fsx и могут содержать директивы #r (ссылки) и #load.

Пример скрипта:

#r "nuget: Newtonsoft.Json"
open Newtonsoft.Json

let data = {| Name = "Alice"; Age = 30 |}
printfn "%s" (JsonConvert.SerializeObject(data))

5. Продвинутые возможности языка

Активные шаблоны (Active Patterns)

Позволяют расширять сопоставление с образцом пользовательской логикой.

Полные активные шаблоны
let (|Even|Odd|) n =
if n % 2 = 0 then Even else Odd

let describe n =
match n with
| Even -> "even"
| Odd -> "odd"
Частичные активные шаблоны
let (|Int|_|) str =
match System.Int32.TryParse(str) with
| (true, n) -> Some n
| _ -> None

match "123" with
| Int n -> printfn "Parsed: %d" n
| _ -> printfn "Not a number"
Параметризованные шаблоны
let (|DivisibleBy|_|) divisor n =
if n % divisor = 0 then Some () else None

match 15 with
| DivisibleBy 5 () -> "Divisible by 5"
| _ -> "Not divisible"

Вычислительные выражения (Computation Expressions)

Обобщённый механизм для создания DSL. Используется в async, seq, task, option, result.

Пример: кастомный OptionBuilder:

type OptionBuilder() =
member _.Bind(x, f) = Option.bind f x
member _.Return(x) = Some x
member _.ReturnFrom(x) = x
member _.Zero() = Some ()

let option = OptionBuilder()

let safeDivide a b =
if b = 0 then None else Some (a / b)

let compute a b c =
option {
let! x = safeDivide a b
let! y = safeDivide x c
return y
}

Вызов: compute 100 10 2Some 5; compute 100 0 2None.

Типы-провайдеры (Type Providers)

Генерируют типы во время компиляции на основе внешних данных (CSV, JSON, API, базы данных).

Пример с CSV:

#r "nuget: FSharp.Data"

open FSharp.Data

type Sales = CsvProvider<"sample.csv">
let sales = Sales.Load("data.csv")
for row in sales.Rows do
printfn "%s: %f" row.Product row.Price

Тип Sales создаётся автоматически, с полями, соответствующими столбцам CSV.

Другие провайдеры:

  • JsonProvider
  • XmlProvider
  • SqlDataProvider (для SQL Server)
  • ODataService

Типы-провайдеры требуют поддержки IDE и работают только в режиме разработки (design-time).

Измерения (Statically Resolved Type Parameters)

Позволяют писать обобщённый код с ограничениями на наличие методов или операторов:

let inline addAndLog x y =
let result = x + y
printfn "Computed: %A" result
result

Тип: 'a -> 'a -> 'a when 'a : (static member (+) : 'a -> 'a -> 'a)

Работает с любыми типами, поддерживающими +.


6. Производительность и оптимизации

  • Хвостовая рекурсия — компилируется в цикл, не потребляет стек.
  • Иммутабельные структуры — безопасны, но могут создавать накладные расходы при частом копировании.
  • Массивы вместо списков — для интенсивной обработки данных.
  • inline — подставляет тело функции, устраняя вызов (полезно для SRTP).
  • Избегание замыканий в горячих циклах — они выделяют память.

7. Тестирование

F# совместим с xUnit, NUnit, FsCheck.

Пример с xUnit:

open Xunit

[<Fact>]
let ``addition works`` () =
Assert.Equal(4, 2 + 2)

FsCheck — генерация случайных данных:

open FsCheck

let revRevIsId xs = List.rev (List.rev xs) = xs
Check.QuickThrowOnFailure revRevIsId

Практические паттерны проектирования, архитектура приложений, работа с файлами, сетью, сериализацией и реальные сценарии использования

1. Архитектурные подходы в F#

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

Onion Architecture / Hexagonal Architecture

  • Ядро приложения — доменные типы и чистые функции.
  • Внешние слои — адаптеры для ввода-вывода (файлы, сеть, базы данных).
  • Зависимости направлены внутрь: инфраструктура зависит от ядра, а не наоборот.

Пример структуры:

Domain/          — доменные типы и логика
Application/ — use cases, сервисы
Infrastructure/ — файлы, HTTP, базы данных
Interfaces/ — интерфейсы для DI (если используется)
Program.fs — точка входа, компоновка

Pipeline-обработка

Данные проходят через цепочку преобразований:

let processOrder =
validateOrder
>> enrichOrder
>> calculateTotal
>> applyDiscount
>> persistOrder

Каждая функция принимает Result<Order, Error> и возвращает Result<Order, Error>, что позволяет легко обрабатывать ошибки.

Модель «Smart Constructors»

Конструкторы типа скрывают прямое создание, обеспечивая инварианты:

module Email =
type T = private Email of string
let create (s: string) =
if System.Text.RegularExpressions.Regex.IsMatch(s, @"^.+@.+\..+$")
then Ok (Email s)
else Error "Invalid email"
let value (Email s) = s

Тип Email.T нельзя создать напрямую — только через Email.create.


2. Обработка ошибок в реальных приложениях

F# поощряет явное моделирование ошибок через Result<'T, 'Error>.

Типизированные ошибки

type OrderError =
| InvalidCustomerId
| OutOfStock of productId: string
| PaymentDeclined

type Result<'T> = Result<'T, OrderError>

Комбинаторы для цепочек

let bind f x =
match x with
| Ok v -> f v
| Error e -> Error e

let (>>=) = bind

Использование:

validateCustomerId id
>>= loadCustomer
>>= checkInventory
>>= chargePayment
>>= confirmOrder

Если любой шаг вернёт Error, цепочка останавливается.

Логирование и восстановление

Можно добавить логирование без нарушения потока:

let logAndContinue onError result =
match result with
| Error e ->
logError e
onError e
| Ok _ -> result

3. Работа с файловой системой

F# использует стандартные .NET API, но с функциональным стилем.

Чтение файла

open System.IO

let readAllLines path =
try
File.ReadAllLines(path) |> Array.toList |> Ok
with
| :? IOException as ex -> Error ex.Message

Безопасная запись

let writeText path content =
async {
try
do! File.WriteAllTextAsync(path, content) |> Async.AwaitTask
return Ok ()
with
| :? IOException as ex -> return Error ex.Message
}

Работа с путями

let configPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "myapp", "config.json")

4. Сетевые операции

HTTP-запросы с HttpClient

open System.Net.Http

let httpClient = new HttpClient()

let fetchJsonAsync (url: string) : Async<string> =
async {
let! response = httpClient.GetAsync(url) |> Async.AwaitTask
if response.IsSuccessStatusCode then
let! content = response.Content.ReadAsStringAsync() |> Async.AwaitTask
return content
else
return failwith $"HTTP {response.StatusCode}"
}

Асинхронный сервер с Suave (лёгкий веб-фреймворк)

open Suave
open Suave.Filters
open Suave.Operators

let app =
choose [
GET >=> path "/hello" >=> Successful.OK "Hello from F#!"
POST >=> path "/echo" >=> Request.streamReader >>= fun body -> Successful.OK body
]

[<EntryPoint>]
let main _ =
WebApp.startWebServer defaultConfig app
0

5. Сериализация и десериализация

JSON с System.Text.Json

open System.Text.Json

type Person = { Name: string; Age: int }

let serialize (p: Person) = JsonSerializer.Serialize(p)
let deserialize (json: string) = JsonSerializer.Deserialize<Person>(json)

Для корректной работы с записями рекомендуется [<CLIMutable>]:

[<CLIMutable>]
type Person = { Name: string; Age: int }

JSON с Newtonsoft.Json

Поддерживает неизменяемые записи без атрибутов:

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

let settings = JsonSerializerSettings(ContractResolver = CamelCasePropertyNamesContractResolver())
let json = JsonConvert.SerializeObject(person, settings)
let person = JsonConvert.DeserializeObject<Person>(json, settings)

CSV с FSharp.Data

type Sales = CsvProvider<"sales.csv">
let data = Sales.Load("actual_sales.csv")
for row in data.Rows do
printfn "%s sold %d units" row.Product row.Units

6. Параллелизм и конкурентность

Параллельное выполнение задач

let urls = ["https://a.com"; "https://b.com"; "https://c.com"]

let fetchAll =
urls
|> List.map fetchUrlAsync
|> Async.Parallel
|> Async.RunSynchronously

Агенты (MailboxProcessor)

Лёгковесный актор для управления состоянием:

type Message = 
| Add of int
| Get of AsyncReplyChannel<int>

let counterAgent = MailboxProcessor.Start(fun inbox ->
let rec loop total =
async {
let! msg = inbox.Receive()
match msg with
| Add n -> return! loop (total + n)
| Get reply ->
reply.Reply(total)
return! loop total
}
loop 0)

// Использование
counterAgent.Post(Add 5)
let current = counterAgent.PostAndReply(fun reply -> Get reply)

7. Тестирование и отладка

Юнит-тесты с xUnit

[<Fact>]
let ``Email creation rejects invalid format`` () =
let result = Email.create "not-an-email"
match result with
| Error _ -> ()
| Ok _ -> Assert.Fail("Should not succeed")

Отладка в VS Code

  • Установите Ionide.
  • Добавьте .vscode/launch.json:
    {
    "version": "0.2.0",
    "configurations": [
    {
    "name": "Launch F#",
    "type": "coreclr",
    "request": "launch",
    "program": "${workspaceFolder}/bin/Debug/net8.0/MyApp.dll",
    "cwd": "${workspaceFolder}"
    }
    ]
    }
  • Ставьте точки останова в .fs файлах.

8. Распространённые сценарии использования F#

Анализ данных

  • Чтение CSV/JSON → фильтрация → агрегация → экспорт.
  • Использование Deedle (библиотека для анализа данных, аналог Pandas).

Микросервисы

  • Чистая бизнес-логика + асинхронный ввод-вывод.
  • Лёгкая сборка в Docker-образ.

Скрипты автоматизации

  • .fsx файлы для обработки логов, генерации отчётов, миграций.

Финансовые расчёты

  • Типы с единицами измерения (USD, EUR).
  • Безопасные вычисления с decimal.

Компиляторы и парсеры

  • Размеченные объединения для AST.
  • Парсеры с FParsec.

Инструменты командной строки, CI/CD, упаковка приложений, Docker, производительность, профилирование и лучшие практики

1. Командная строка и .NET CLI

F# полностью интегрирован в экосистему .NET. Основные команды:

Создание проекта

dotnet new console -lang F# -n MyApp
dotnet new classlib -lang F# -n MyLib
dotnet new web -lang F# -n MyWebApp # требует шаблонов (например, Giraffe)

Управление зависимостями

Добавление пакета:

dotnet add package FSharp.Data --version 6.4.0

Установка глобального инструмента (например, FAKE):

dotnet tool install fake-cli --global

Сборка и запуск

dotnet build
dotnet run
dotnet publish -c Release -r linux-x64 --self-contained true

Флаги публикации:

  • -c Release — оптимизированная сборка
  • -r linux-x64 — целевая платформа (win-x64, osx-arm64 и т.д.)
  • --self-contained true — включает .NET runtime в дистрибутив

Результат публикации — автономный исполняемый файл или папка с приложением.


2. Непрерывная интеграция и доставка (CI/CD)

GitHub Actions

Пример .github/workflows/ci.yml:

name: Build and Test

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal

Для публикации артефактов:

    - name: Publish
run: dotnet publish -c Release -o ./publish
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: myapp
path: ./publish/

Azure DevOps, GitLab CI

Аналогичные шаги: восстановление, сборка, тестирование, публикация.


3. Упаковка и распространение

Самодостаточные приложения

Команда:

dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true

Параметры:

  • PublishSingleFile=true — объединяет всё в один .exe
  • IncludeNativeLibrariesForSelfExtract=true — ускоряет первый запуск

Результат: один исполняемый файл (~70–100 МБ), не требующий установки .NET.

NuGet-пакеты

Создание библиотеки как NuGet-пакета:

В .fsproj:

<PropertyGroup>
<PackageId>MyFSharpLibrary</PackageId>
<Version>1.0.0</Version>
<Authors>YourName</Authors>
<Description>A useful F# library</Description>
<PackageProjectUrl>https://github.com/you/MyLib</PackageProjectUrl>
<RepositoryUrl>https://github.com/you/MyLib</RepositoryUrl>
</PropertyGroup>

Сборка пакета:

dotnet pack -c Release

Публикация:

dotnet nuget push bin/Release/MyFSharpLibrary.1.0.0.nupkg --api-key YOUR_KEY --source https://api.nuget.org/v3/index.json

4. Контейнеризация с Docker

Минимальный Dockerfile

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-self-contained

FROM mcr.microsoft.com/dotnet/runtime:8.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["./MyApp"]

Для самодостаточного приложения:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -o /app

FROM scratch
COPY --from=build /app/MyApp /MyApp
ENTRYPOINT ["/MyApp"]

Образ на базе scratch занимает всего ~30–50 МБ.

Запуск

docker build -t myapp .
docker run --rm myapp

5. Производительность и профилирование

Советы по производительности

  • Используйте массивы вместо списков для больших объёмов данных.
  • Избегайте частого создания замыканий в циклах.
  • Применяйте inline для маленьких функций с SRTP.
  • Включайте оптимизации: <Optimize>true</Optimize> в .fsproj.
  • Используйте Span<T> и Memory<T> для работы с буферами без выделения памяти (через System.Memory).

Профилирование

  • Visual Studio Profiler — CPU, память, горячие пути.

  • dotTrace / dotMemory от JetBrains — детальный анализ.

  • perfcollect (Linux):

    sudo perfcollect collect MySession
    # после завершения
    sudo perfcollect view MySession
  • BenchmarkDotNet — микро-бенчмарки:

    open BenchmarkDotNet.Attributes
    open BenchmarkDotNet.Running

    type Bench() =
    let data = [|1..10000|]

    [<Benchmark>]
    member _.ListSum() = data |> Array.toList |> List.sum

    [<Benchmark>]
    member _.ArraySum() = Array.sum data

    BenchmarkRunner.Run<Bench>() |> ignore

6. Лучшие практики разработки на F#

Стиль кода

  • Используйте иммутабельность по умолчанию.
  • Предпочитайте выражения операторам.
  • Избегайте null; используйте Option.
  • Не используйте исключения для управления потоком; применяйте Result.
  • Делайте функции чистыми (без побочных эффектов) там, где возможно.

Именование

  • Типы, модули, пространства имён — PascalCase.
  • Функции, значения — camelCase.
  • Активные шаблоны — PascalCase внутри (|...|).

Обработка ошибок

  • Моделируйте возможные состояния явно через типы.
  • Используйте Error с конкретными случаями, а не строками.
  • Цепочки обработки — через bind или вычислительные выражения.

Тестирование

  • Покрывайте чистые функции юнит-тестами.
  • Используйте property-based testing (FsCheck) для инвариантов.
  • Тестируйте граничные случаи: пустые списки, нулевые значения, переполнения.

Документирование

  • Используйте XML-комментарии:

    /// <summary>
    /// Calculates the factorial of a non-negative integer.
    /// </summary>
    /// <param name="n">Input number (must be >= 0)</param>
    /// <returns>The factorial of n</returns>
    let rec factorial n = ...
  • Генерируйте документацию:

    <GenerateDocumentationFile>true</GenerateDocumentationFile>

7. Экосистема и популярные библиотеки

КатегорияБиблиотекаНазначение
ВебGiraffe, Saturn, SuaveФункциональные веб-фреймворки
HTTP-клиентHttp.fs, FSharp.HttpЛёгкие клиенты поверх HttpClient
JSONNewtonsoft.Json, System.Text.JsonСериализация
CSV/HTML/XMLFSharp.DataТипы-провайдеры
ПарсингFParsecКомбинаторный парсер
ТестированиеFsCheck, ExpectoProperty-based и assertion-тесты
СборкаFAKEАвтоматизация сборки на F#
Машинное обучениеML.NET, DiffSharpИнтеграция с .NET ML и автоматическое дифференцирование
GUIFabulous (для Xamarin), Avalonia + ElmishКроссплатформенные интерфейсы

Версии языка, совместимость, миграция, поддержка платформ и ресурсы

1. Версии F# и их ключевые особенности

F# развивается в рамках .NET и публикуется как часть SDK. Каждая версия F# привязана к определённой версии .NET.

Версия F#ГодОсновные возможности
F# 1.02005Базовый функционал: записи, размеченные объединения, сопоставление с образцом
F# 2.02010Включение в Visual Studio; асинхронные рабочие процессы (async)
F# 3.02012Типы-провайдеры, единицы измерения, улучшенный вывод типов
F# 4.02015Улучшения в интеропе с C#, nameof, open на уровне класса
F# 4.12017[<Struct>] для размеченных объединений и записей, byref, Result в ядре
F# 4.52018Span-безопасность, fixed выражения, улучшенная производительность
F# 4.72019Неявные операторы преобразования, улучшенный синтаксис для списков
F# 5.02020Интерполяция строк ($""), nameof, open static, улучшения в FSI
F# 6.02021task вычислительные выражения, try/with в выражениях, улучшенный синтаксис для кортежей
F# 7.02022Улучшенная поддержка атрибутов, required поля в записях, лучшая интеграция с C# 11
F# 8.02023Обязательные поля в записях без инициализации, улучшенная работа с null, Discriminated Unions как struct по умолчанию в некоторых контекстах

Примечание: Начиная с .NET 5, версии F# и .NET синхронизированы: .NET 6 → F# 6.0, .NET 7 → F# 7.0, .NET 8 → F# 8.0.


2. Совместимость между версиями

  • Обратная совместимость: код, написанный на F# 4.x, компилируется на F# 8.0 без изменений.
  • Прямая совместимость отсутствует: использование новых возможностей (например, task { }) требует F# 6.0+.
  • FSharp.Core: основная библиотека времени выполнения. Рекомендуется использовать последнюю стабильную версию, совместимую с целевой платформой.

В .fsproj явно указывается зависимость:

<PackageReference Update="FSharp.Core" Version="8.0.401" />

Это позволяет использовать новые API даже при таргетинге на старые версии .NET (в пределах разумного).


3. Поддержка платформ

F# работает на всех платформах, поддерживаемых .NET:

ПлатформаПоддержкаОсобенности
.NET 8 / .NET 7 / .NET 6ПолнаяРекомендуемые целевые платформы
.NET Standard 2.1ПолнаяДля библиотек, совместимых с .NET Core 3.0+
.NET Framework 4.8ЧастичнаяТребуется FSharp.Core 4.7 или ниже; нет поддержки Span<T>, task и других современных функций
MonoПолнаяИспользуется в Xamarin, Unity (ограниченно)
WebAssembly (Blazor)ПолнаяЧерез .NET 6+ и Blazor WebAssembly
Linux / macOS / WindowsПолнаяОдин и тот же код компилируется без изменений

Рекомендация: для новых проектов используйте .NET 8 и F# 8.0.


4. Миграция проектов

С .NET Framework на .NET 8

  1. Создайте новый проект:
    dotnet new console -lang F# -n MyMigratedApp
  2. Перенесите файлы .fs в правильном порядке.
  3. Обновите зависимости:
    • Замените packages.config на PackageReference.
    • Обновите NuGet-пакеты до версий, совместимых с .NET 8.
  4. Удалите устаревшие API:
    • Async.StartAsync.StartImmediate или Task.Run
    • List.toArrayList.toArray (остался, но проверьте типы)
  5. Включите современные флаги:
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <AnalysisLevel>latest</AnalysisLevel>

С F# 4.x на F# 8.0

  • Замените async.Return на return в async { }.
  • Используйте task { } вместо Async.AwaitTask >> Async.RunSynchronously для взаимодействия с C#.
  • Замените failwith в горячих путях на Result.Error, если возможно.
  • Обновите синтаксис строк: "Hello " + name$"Hello {name}".

5. Интеграция с другими языками .NET

Из C# в F#

  • F# сборки видны в C# как обычные .NET-библиотеки.
  • Записи с [<CLIMutable>] работают как классы с публичными свойствами.
  • Размеченные объединения доступны как абстрактные базовые классы с наследниками.

Из F# в C#

  • Используйте [<CompiledName("MyMethod")>] для контроля имени метода.
  • Помечайте модули как [<RequireQualifiedAccess>], чтобы избежать конфликтов имён.
  • Для коллекций используйте ResizeArray<T> (синоним List<T> из .NET), если нужна мутабельность.