- Введение: зачем F# и чем он отличается от C#
- Функциональное мышление: ключевые принципы
- Синтаксис и базовые конструкции
- Типы и типовая система
- Функции: определение, частичное применение, каррирование
- Иммутабельность и работа с состоянием
- Коллекции и функции высшего порядка
- Модули, пространства имён и организация кода
- Управление потоком: сопоставление с образцом (pattern matching)
- Вариативные типы: Option, Result, Discriminated Unions
- Записи, структуры и классы (Record, Struct, Class)
- Активные шаблоны (Active Patterns)
- Пайпинг и композиция функций
- Вычисляемые выражения (Computation Expressions)
- Асинхронность и параллелизм
- Работа с побочными эффектами
- Мутируемые структуры: когда и как
- Межъязыковая совместимость с C#
- Тестирование и свойствное тестирование
- Практические идиомы F#
- Инструменты и экосистема
- Cheat Sheet: быстрый справочник по синтаксису и паттернам
- F# — это функционально-первый язык на .NET. Он поддерживает ООП и императивный стиль, но поощряет функциональные практики.
- Основные плюсы:
- Меньше кода для выражения сложной логики.
- Безопасность типов и иммутабельность по умолчанию.
- Сопоставление с образцом и варианты типов сильно упрощают обработку состояний.
- Прекрасно подходит для доменного моделирования, трансформаций данных, конвейеров, DSL, научных вычислений.
- Отличия от C#:
- Код выражение-ориентирован: почти всё — выражения, возвращающие значения.
- Иммутабельность по умолчанию; переменные — это значения.
- Сопоставление с образцом вместо каскадов if/else/switch.
- DU (Discriminated Unions) вместо наследования для моделирования вариантов.
- Более лаконичный синтаксис, меньше «шумов» (скобок, точек с запятой).
- Иммутабельность: данные не меняются, создаются новые версии.
- Чистые функции: результаты зависят только от входов, без побочных эффектов.
- Функции как значения: передача функций, возвращение функций, композиция.
- Декларативность: описываем «что» делать, а не «как».
- Типы как спецификация домена: DU и Record позволяют формализовать бизнес-правила на уровне типов.
- Значения по умолчанию неизменяемые.
let x = 42 // Иммутабельное значение
let y = x + 1 // Новое значение на основе xlet add a b = a + b // Функция add принимает два аргумента
let inc = add 1 // Частичное применение: inc b = add 1 b
inc 10 // => 11- Нет скобок вокруг аргументов по умолчанию; пробел — оператор применения.
- Возвращаемое значение — последнее выражение в теле функции.
let result =
let a = 10
let b = 5
a * b // Последнее выражение — возвращаемоеlet abs x =
if x >= 0 then x
else -x- Типы выводятся автоматически (type inference), но можно аннотировать:
let add (a:int) (b:int) : int = a + b-
Базовые типы: int, float, bool, string, decimal, char, unit (аналог void, но значение — () )
-
Кортежи (tuples):
let pair = (1, "a")
let first, second = pair // Деконструкция- Списки: неизменяемые, односвязные.
let xs = [1;2;3]
let ys = 0 :: xs // Препенд (константный)- Массивы: изменяемые, фикс-тип, индексируемые.
let arr = [|1;2;3|]
arr.[0] <- 42 // Мутация- Словари/карты: Map (иммутабельная), Dictionary (из BCL, изменяемая).
let m = Map.empty.Add("a", 1).Add("b", 2)
m.TryFind "a" // Option<int>- В F# функция многопараметрична на самом деле каррирована: [f: A -> B -> C].
- Частичное применение создаёт новые функции.
let multiply a b = a * b // multiply: int -> int -> int
let double = multiply 2 // double: int -> int
double 21 // => 42- Анонимные функции:
let squares = List.map (fun x -> x * x) [1;2;3]- Функции высшего порядка: принимают/возвращают функции.
let applyTwice f x = f (f x)
applyTwice ((+) 1) 10 // => 12- Значения неизменяемые: вместо мутаций — создание новых.
- Для сложных сценариев состояния — использовать DU + Record или вычисляемые выражения.
type Counter = { value:int }
let inc c = { c with value = c.value + 1 }
let start = { value = 0 }
start |> inc |> inc // => { value = 2 }- Основные модули: List, Array, Seq (ленивые последовательности).
let xs = [1..5] // [1;2;3;4;5]
xs |> List.filter (fun x -> x%2=0) // [2;4]
xs |> List.map (fun x -> x*x) // [1;4;9;16;25]
xs |> List.fold (fun acc x -> acc + x) 0 // 15- Seq — ленивые:
let squares = seq { for i in 1..5 do yield i*i }
squares |> Seq.take 3 |> Seq.toList // [1;4;9]- Композиция преобразований:
let process =
List.filter (fun x -> x > 10)
>> List.map (fun x -> x * 2)
[5;12;20] |> process // [24;40]- namespace — для группировки модулей; module — для функций/типов.
namespace MyApp
module Math =
let add a b = a + b
let square x = x * x
module StringUtils =
let trim (s:string) = s.Trim()- Открытие модулей:
open MyApp.Math
add 2 3 // 5- Файлы в проекте F# компилируются в порядке, указанном в проекте; определения должны быть доступны сверху вниз.
- Основной механизм ветвления, безопасный и выразительный.
let describeNumber x =
match x with
| 0 -> "zero"
| 1 | 2 -> "one or two" // Альтернатива
| n when n < 0 -> "negative" // Guard
| _ -> "many"- Сопоставление на кортежах/записях:
let comparePair (a,b) =
match a,b with
| x,y when x = y -> "equal"
| _,_ -> "different"- [Option<'T>] — безопасное отсутствие значения: Some x | None
let tryDivide a b =
if b = 0 then None else Some (a / b)
match tryDivide 10 2 with
| Some q -> sprintf "Result %d" q
| None -> "Division by zero"- [Result<'T,'Err>] — успех или ошибка: Ok x | Error e
type Error = | NotFound | InvalidInput of string
let readUser id =
if id > 0 then Ok (sprintf "User%d" id)
else Error (InvalidInput "id must be positive")
match readUser 1 with
| Ok u -> sprintf "Hello %s" u
| Error NotFound -> "No user"
| Error (InvalidInput msg) -> msg- Моделируют доменные варианты вместо наследования.
type PaymentMethod =
| Cash
| Card of number:string * holder:string
| Transfer of iban:string
let pay pm amount =
match pm with
| Cash -> sprintf "Cash: %M" amount
| Card (n,h) -> sprintf "Card %s (%s): %M" n h amount
| Transfer iban -> sprintf "IBAN %s: %M" iban amount- Имутабельные типы с именованными полями; поддерживают копирование с изменением.
type Person = { Name:string; Age:int }
let john = { Name = "John"; Age = 30 }
let olderJohn = { john with Age = 31 }- Для значимых типов и оптимизации аллокаций.
[<Struct>]
type Point = { X:int; Y:int }- F# поддерживает классы, конструкторы, методы, свойства.
type Counter(initial:int) =
let mutable value = initial
member _.Inc() = value <- value + 1
member _.Value = value
let c = Counter(0)
c.Inc(); c.Value // 1- Позволяют создавать настраиваемые «паттерны» для match.
let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd
let describe n =
match n with
| Even -> "even"
| Odd -> "odd"- Параметризованные:
let (|StartsWith|_|) (prefix:string) (s:string) =
if s.StartsWith(prefix) then Some s else None
match "FSharp" with
| StartsWith "F" s -> sprintf "%s starts with F" s
| _ -> "no"- Пайпинг [|>] передаёт результат слева в функцию справа.
let normalize =
String.trim
>> String.lowercase // условно: представьте функцию
" Hello " |> normalize- В F#: чаще используем модули, напр. [String] из FSharp.Core.Additional (или пишем свои).
Пример с реальными функциями:
let toLower (s:string) = s.ToLowerInvariant()
let trim (s:string) = s.Trim()
" Hello " |> trim |> toLower // "hello"- Композиция [>>] и обратная композиция [<<]
let normalize = trim >> toLower
normalize " HI " // "hi"- Синтаксический сахар для «монадоподобных» контекстов: async, seq, option, task и др.
- Пример: seq
let items = seq {
yield 1
yield! [2;3] // Вставка другой последовательности
for i in 4..5 do
yield i
}
items |> Seq.toList // [1;2;3;4;5]- Пример: option (из библиотеки, либо собственной реализации)
let option =
// Псевдопример, если есть билдер option
// позволяет писать let! для извлечения Some
()- Пример: async
let fetchAsync url = async {
// имитация асинхронного вызова
do! Async.Sleep 10
return sprintf "data from %s" url
}
let program = async {
let! a = fetchAsync "A"
let! b = fetchAsync "B"
return a + " & " + b
}
Async.RunSynchronously program- F# предоставляет [async] вычислимые выражения и функции в [Async] модуле.
let work i = async {
do! Async.Sleep 50
return i*i
}
[1..5]
|> List.map work
|> Async.Parallel // запускает параллельно
|> Async.RunSynchronously
// => [|1;4;9;16;25|]- Можно использовать [task] (из .NET) и [Task]-билдеры в современных версиях.
- Принцип: локализовать эффекты на границах.
- Логика чистая, ввод/вывод — на периферии (например, в «порт/адаптер» слоях).
- Для тестируемости — передавать зависимость как функцию/интерфейс, DU моделировать результат.
- Иногда мутация эффективнее (буферы, счётчики, внутренние циклы).
- Используйте mutable или ref, но ограничивайте область видимости.
let sum arr =
let mutable total = 0
for x in arr do
total <- total + x
total- [ref] — ячейка с изменяемым значением:
let r = ref 0
r := !r + 1
!r // 1- F# и C# свободно вызывают код друг друга в рамках .NET.
- Распространённые моменты:
- DU компилируются в классы с вложенными типами — удобно читать из C#, но менее естественно.
- Record — иммутабельные классы с авто-свойствами; из C# видны как типы с get-only и WithMembers через методы.
- Для удобства публичного API, иногда добавляют [CLIMutable] атрибут к Record для поддержания set-свойств (например, сериализация).
[<CLIMutable>]
type User = { Name:string; Age:int }- xUnit/NUnit подходят.
- Для свойствного тестирования используйте FsCheck.
Пример простой property:
// Свойство: реверс дважды даёт исходный список
let revTwiceIsOriginal (xs:int list) =
(List.rev (List.rev xs)) = xs- «Railway oriented programming»: использования Result для конвейера ошибок.
let bind f res =
match res with
| Ok x -> f x
| Error e -> Error e
let map f res =
match res with
| Ok x -> Ok (f x)
| Error e -> Error e
let validatePositive x =
if x > 0 then Ok x else Error "must be positive"
let toString x = Ok (string x)
let pipeline =
validatePositive
>> bind toString
pipeline 10 // Ok "10"
pipeline -1 // Error "must be positive"- Доменно-ориентированное проектирование через DU + Record вместо иерархий классов.
- Минимизировать null: использовать Option, Result.
- Использовать сопоставление с образцом для всеобъемлющей обработки вариантов.
- Компилятор: входит в .NET SDK, проект F# — .fsproj.
- IDE:
- Visual Studio
- JetBrains Rider
- VS Code + Ionide (очень популярно в F# сообществе)
- Пакеты:
- FSharp.Core — стандартная библиотека.
- FsCheck — свойствное тестирование.
- FSharp.Data — CSV, JSON, XML, HTTP.
- FAKE — сборка.
- Saturn, Giraffe — веб-фреймворки.
- Документация: fsharp.org, официальные туториалы.
let x = 1
let f a b = a + b
let g = f 10 // частичное применение
let inline sq x = x * x // inline для перформанса/обобщенияtype Person = { Name:string; Age:int }
type Payment = Cash | Card of string | Transfer of string
type Tree<'T> = Leaf of 'T | Node of Tree<'T> * Tree<'T>match value with
| Some x -> ...
| None -> ...xs |> List.map f |> List.filter p
let h = f >> g >> klet xs = [1;2;3]
let arr = [|1;2;3|]
let m = Map [ ("a",1); ("b",2) ]let s = seq { for i in 1..10 do yield i*i }
s |> Seq.take 3 |> Seq.toListlet tryParseInt s =
match System.Int32.TryParse s with
| true, v -> Some v
| _ -> None
let divide a b =
if b=0 then Error "zero" else Ok (a/b)let work i = async { do! Async.Sleep 10; return i*i }
[1..5] |> List.map work |> Async.Parallel |> Async.RunSynchronouslylet mutable count = 0
count <- count + 1namespace Demo
module Math =
let add a b = a + b- Добавляйте [CLIMutable] к записям для сериализации.
- DU и Record доступны из C#, но могут выглядеть непривычно.