F# — функциональный язык программирования для платформы .NET, который компилируется в общий промежуточный код и работает бок о бок с C#. Он появился как исследовательский проект Microsoft Research, но давно стал полноценной частью .NET SDK: проекты собираются через dotnet, пакеты берутся из NuGet, а библиотеки совместимы с остальной экосистемой. F# не требует полностью менять мышление: можно писать императивный код, но язык поощряет неизменяемые данные, сопоставление с образцом и композицию функций. Ниже — практический маршрут от установки до тестов и производительности.
Чем F# отличается от C#
- Неизменяемость по умолчанию: привязку let нельзя переприсвоить без ключевого слова mutable.
- Размеченные объединения описывают состояние предметной области без иерархий классов.
- Сопоставление с образцом проверяет полноту на этапе компиляции.
- Вывод типов уменьшает шум, но не отключает проверку.
- Функции — значения: их можно передавать, возвращать и частично применять.
- Скрипты .fsx выполняются через dotnet fsi без создания проекта.
При этом F# не изолирован: Task, Span<T>, HttpClient и любые NuGet-пакеты доступны напрямую. Это делает его удобным для обработки данных, парсеров, предметных моделей и серверных сервисов.
Установка и структура проекта
Отдельный установщик не нужен: достаточно .NET SDK, в котором уже есть компилятор F# и интерактивная среда. Проверьте окружение и создайте консольный проект.
dotnet --info
dotnet new console -lang "F#" -o demo
cd demo
dotnet run
В отличие от C#, порядок файлов в проекте важен: компилятор обрабатывает их сверху вниз, поэтому зависимости должны быть объявлены раньше. В файле проекта это видно явно.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Include="Domain.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
</Project>
Записи, объединения и сопоставление
Предметную модель удобно описывать записями и размеченными объединениями. Компилятор сам выводит типы полей, а match заставляет обработать все варианты.
type Status =
| Active
| Suspended of reason: string
type Account = {
Id: int
Name: string
Status: Status
}
let describe account =
match account.Status with
| Active -> $"{account.Name}: активен"
| Suspended reason -> $"{account.Name}: заблокирован ({reason})"
Если добавить новый вариант, старый код перестанет компилироваться — это и есть главная выгода. В C# похожего эффекта добиваются sealed-иерархиями и switch-выражениями, но F# делает это идиомой по умолчанию.
Option и Result вместо исключений
Для ожидаемых ошибок используют Option<T> и Result<T, TError>. Исключения остаются для действительно исключительных ситуаций, а не для «файл не найден».
let parsePort (value: string) =
match System.Int32.TryParse value with
| true, port when port > 0 && port < 65536 -> Ok port
| _ -> Error "порт должен быть в диапазоне 1–65535"
let port =
parsePort "8080"
|> Result.defaultWith (fun message -> failwith message)
Такой код легко тестировать: не нужно ловить исключения, достаточно сравнить результат с ожидаемым значением. Для коллекций применяйте List.choose и Array.choose, чтобы отбросить неудачные элементы без вложенных условий.
Асинхронность
В современном F# основной инструмент — task, который напрямую работает с Task и дружит с асинхронными API .NET. Вычисление возвращает задачу, а let! ждёт результат, не блокируя поток.
open System.Net.Http
let fetch (url: string) = task {
use client = new HttpClient()
let! response = client.GetAsync(url)
response.EnsureSuccessStatusCode() |> ignore
return! response.Content.ReadAsStringAsync()
}
Старый async { } тоже поддерживается и остаётся полезным для отмены и композиции, но в новом коде чаще выбирают task. Не смешивайте два стиля в одном модуле без необходимости: это усложняет чтение.
Скрипты и пакеты
Для разовых задач проект не нужен. Интерактивная среда dotnet fsi выполняет файлы .fsx, а директивы #r подключают NuGet-пакеты прямо в скрипте.
#r "nuget: FSharp.Data"
open FSharp.Data
let page = Http.RequestString("https://example.com")
printfn "%d" page.Length
Скрипт запускается командой dotnet fsi script.fsx. Это удобно для проверки гипотез, разбора логов и небольших миграций, но в продакшен такой код переносят в проект с тестами и фиксированными версиями зависимостей.
Тесты и качество кода
Тестовый проект создаётся тем же шаблоном, что и в C#. Форматирование лучше отдать Fantomas, а предупреждения — сделать ошибками в CI.
dotnet new xunit -lang "F#" -o tests
dotnet add tests package FsUnit.xUnit
dotnet tool install -g fantomas
dotnet fantomas .
dotnet test
Компилятор F# предупреждает о неполном сопоставлении (FS0025) и о неиспользуемых значениях. В CI полезно включить -warnaserror, иначе предупреждения незаметно накопятся. Включённый Nullable добавляет проверку ссылочных типов, пришедших из C#-библиотек.
Производительность и типичные ошибки
- Списки F# — связные, вставка в начало дешёвая, доступ по индексу — линейный; для больших наборов берите
ArrayилиResizeArray. - Хвостовая рекурсия оптимизируется, но не любую рекурсию компилятор превращает в цикл.
[<Struct>]убирает выделение памяти для маленьких записей, но копирует данные.- Закрытия и
Seqсоздают промежуточные объекты; в горячем цикле измеряйте, а не угадывайте. - Порядок файлов в fsproj — самая частая причина «unknown identifier».
- Публикация:
dotnet publish -c Release -r linux-x64; self-contained увеличивает размер, но убирает зависимость от установленного рантайма.
Чек-лист
- Версия SDK зафиксирована в global.json.
- Модель предметной области построена на записях и объединениях, а не на классах с сеттерами.
- Ожидаемые ошибки возвращаются через Result, исключения остаются для сбоев.
- В CI включены тесты, Fantomas и warnaserror.
- Скрипты .fsx не попали в продакшен без тестов.
- Производительность проверена на реальном объёме данных.
Итог
F# даёт компактную и строгую модель для задач, где важны корректность и преобразование данных. Начать проще с одного проекта и интерактивной среды: опишите типы, замените ветвления сопоставлением, вынесите ошибки в Result. .NET SDK, NuGet и общая инфраструктура означают, что язык не запирает в отдельной экосистеме. Главное — не тащить идиомы C# механически: неизменяемость, композиция и явные типы работают лучше, когда им следуют последовательно.