Сопоставление с образцом в F# — практикум
Роль match в F#
В C# или Java ветвление чаще всего делают через if / else или switch по одному дискриминатору. В F# центральная конструкция — match выражение with: вы берёте одно значение и описываете, что делать для каждой возможной формы этого значения.
Сопоставление с образцом (pattern matching) умеет:
- сравнивать с конкретным литералом (
0,"ok"); - разбирать структуру (голова и хвост списка, поля записи, вариант объединения);
- добавлять условие
whenк шаблону; - отлавливать «всё остальное» символом
_.
Компилятор проверяет исчерпывающность: для многих типов (размеченные объединения, bool, часть кортежей) каждый случай должен быть обработан. Забытая ветка — предупреждение или ошибка на этапе сборки, а не сюрприз в runtime.
Теория типов и доменная модель — в F# в экосистеме .NET. Здесь — приёмы с разбором строк кода.
Предварительно: Интерактивная работа с F# — удобно копировать примеры в FSI и смотреть типы.
Анатомия match
Общий вид:
match <что сравниваем> with
| <шаблон1> -> <результат1>
| <шаблон2> -> <результат2>
| ...
match ... with— ключевые слова; между ними — выражение (число, список, запись,Option, и т.д.).- Каждая ветка начинается с
|(вертикальная черта). ->отделяет шаблон от результата ветки (в F# это выражение, у него есть значение и тип).- Все ветки одного
matchдолжны давать совместимый тип результата (например, всеstring).
match — выражение, как if в F#. Его можно вставить в let:
let label = match n with
| 0 -> "zero"
| _ -> "nonzero"
Литералы, when и _
let sign n =
match n with
| 0 -> "zero"
| n when n > 0 -> "positive"
| _ -> "negative"
Построчно:
| Строка | Смысл |
|---|---|
| 0 -> "zero" | Если n равно нулю — вернуть "zero" |
| n when n > 0 | Любое n, для которого выполняется условие n > 0 |
| _ -> "negative" | Все оставшиеся случаи (отрицательные числа) |
when — дополнительный фильтр к шаблону. Без него пришлось бы писать отдельные ветки для каждого положительного числа, что невозможно.
_ — шаблон «любое значение, имя не важно». Обычно стоит последним, иначе перехватит случаи, которые вы хотели обработать отдельно.
Когда хватает одного условия «да/нет», допустим if. match выигрывает, когда вариантов несколько и они связаны со структурой данных (список, DU, Option).
Списки
Список F# ('a list) — цепочка ячеек: голова (первый элемент) и хвост (остаток списка). Пустой список — []. Добавление в начало: голова :: хвост (оператор ::, читают «cons»).
let rec sumList xs =
match xs with
| [] -> 0
| head :: tail -> head + sumList tail
| Шаблон | Что означает |
|---|---|
[] | Список пуст — сумма 0 |
head :: tail | Есть первый элемент head и список tail; сумма = head + сумма хвоста |
rec у let — функция рекурсивная, она вызывает сама себя. Базовый случай [] останавливает рекурсию.
Другие полезные шаблоны:
match xs with
| [] -> "empty"
| [a; b] -> $"exactly two: {a} and {b}"
| _ :: _ :: _ -> "at least two elements"
| [_] -> "one element"
| _ -> "other"
Порядок веток важен: более конкретные шаблоны ([a; b]) ставят выше общих (_ :: _ :: _).
В учебном коде сумму списка часто пишут через List.sum или List.fold; рекурсивный match остаётся эталоном для понимания структуры данных.
Записи и кортежи
Запись — именованный набор полей:
type Order = { Id: int; Amount: decimal; Paid: bool }
let describe { Id = id; Paid = paid } =
if paid then $"Order {id} closed" else $"Order {id} pending"
В параметре функции { Id = id; Paid = paid } — шаблон записи: из значения Order извлекаются поля Id и Paid. Остальные поля (Amount) можно опустить.
Кортеж — упорядоченная пара (или тройка) без имён полей на уровне типа:
let swap (a, b) = (b, a)
let formatPoint (x, y) =
match (x, y) with
| (0, 0) -> "origin"
| (0, _) -> "on Y axis"
| (_, 0) -> "on X axis"
| _ -> "elsewhere"
Здесь сопоставляют пару чисел сразу: сначала особые случаи осей и начала координат, затем общий _.
Размеченные объединения (DU)
Discriminated union — тип «одно из нескольких вариантов», у каждого варианта могут быть данные:
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
let area shape =
match shape with
| Circle r -> System.Math.PI * r * r
| Rectangle (w, h) -> w * h
Circle r— вариант «круг»;r— радиус в этой ветке.Rectangle (w, h)— прямоугольник; кортеж ширины и высоты.
Если позже добавить | Triangle of base: float * height: float и забыть ветку в area, компилятор предупредит: обработаны не все варианты. В switch по enum в C# такой проверки нет.
DU — основной способ моделировать состояния заказа, платежа, результата операции вместе с полями (см. также Option и Result ниже).
Option — «значение есть» или «нет»
Тип Option<'T>:
Some значение— данные есть;None— данных нет (аналог «пусто», но типобезопасно).
let parsePositive (s: string) =
match System.Int32.TryParse s with
| true, n when n > 0 -> Some n
| _ -> None
let printIfSome opt =
match opt with
| Some v -> printfn "%d" v
| None -> printfn "no value"
Int32.TryParse в .NET возвращает пару (успех, число). В первой ветке шаблон true, n означает: парсинг удался, число положительное — возвращаем Some n. Иначе — None.
Для «значения может не быть» в чистом F# предпочитают None, а не null (null в основном при вызове старых .NET API).
Цепочки «попробовать несколько шагов» позже оформляют через Option.bind или вычислительные выражения (Справочник); для чтения кода базой остаётся явный match.
Result — успех или ошибка с причиной
type AppError = | NotFound | InvalidInput
let load id =
if id <= 0 then Error InvalidInput
else Ok $"item-{id}"
let handle result =
match result with
| Ok data -> printfn "ok: %s" data
| Error NotFound -> printfn "missing"
| Error InvalidInput -> printfn "bad id"
Ok data— операция удалась, внутриdata.Error e— ошибка; типeможет быть своим DU (AppError), строкой, исключением — как спроектируете.
Result удобен, когда нужно различать причины сбоя и обрабатывать их в одном месте без глубоких try/catch.
Массивы
let firstOrZero (arr: int[]) =
match arr.Length with
| 0 -> 0
| _ -> arr.[0]
arr.[0] — доступ по индексу (точка перед скобками). Для больших объёмов данных чаще используют Seq или Array с функциями модуля, но match по длине остаётся наглядным.
Сравнение — if, match, function
// function — сокращение: match на единственном аргументе
let sign2 = function
| 0 -> "zero"
| n when n > 0 -> "positive"
| _ -> "negative"
function эквивалентно fun x -> match x with ... — удобно для коротких обработчиков в List.map.
Частые ошибки
| Симптом | Причина | Что сделать |
|---|---|---|
Предупреждение о неполном match | Не все варианты DU | Добавить ветки для каждого | Case |
| Ветка «никогда не сработает» | Общий _ или широкий шаблон выше узкого | Поднять специфичные ветки |
null в доменной логике | Привычка из C# | Использовать Option / Result |
| Дублирование кода в ветках | Копипаст правой части | Вынести let до match или helper-функцию |
| Runtime ошибка на пустом списке | Обращение к List.head без проверки | Сначала match на [] / ::_ |
Активные шаблоны (кратко)
Если один и тот же разбор повторяется (например, строка email или диапазон оценок), его выносят в активный шаблон:
let (|PositiveInt|_|) s =
match System.Int32.TryParse s with
| true, n when n > 0 -> Some n
| _ -> None
let label s =
match s with
| PositiveInt n -> $"value {n}"
| _ -> "not a positive int"
(|PositiveInt|_|) — имя шаблона; Some n — совпадение, None — нет. На старте достаточно обычного match; активные шаблоны — когда логика переиспользуется в десятках мест. Подробнее — Справочник по F#.
Практика в FSI
- Скопируйте
type Shapeиareaвdotnet fsi. - Вызовите
area (Circle 2.0)иarea (Rectangle (3.0, 4.0)). - Добавьте вариант
Triangle— прочитайте предупреждение компилятора. - Перепишите
parsePositiveи вызовите с"10","0","abc".
Дальше
- Императивные конструкции и точка входа — циклы и
mutableрядом сmatch - ООП в F# для взаимодействия с .NET
- Справочник языка F# (Learn) — полный перечень тем по
match - Первая программа на F# — меню на
matchв консоли
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Платформа .NET - архитектура экосистемы, инструменты разработки и модель выполнения приложений. Эти механизмы позволили реализовать фундаментальный принцип .NET — язык — это синтаксический фасад над общей семантикой CLR. Понимание архитектуры .NET невозможно без хронологического контекста, поскольку многие текущие решения — это результат многолетней итеративной оптимизации. Типы приложений на платформе .NET - веб, desktop, мобильные и облачные сценарии в единой экосистеме. Сборка и развёртывание .NET-приложений - артефакты, среды выполнения и практики доставки в продакшен. Пакеты и зависимости в .NET - управление версиями, восстановление пакетов и интеграция в процесс сборки. В Visual Studio проект — это единица сборки — он определяет, что и как компилируется. Проект содержит .csproj, исходные файлы, ресурсы и метаданные зависимостей. NuGet - система управления пакетами .NET для публикации, версионирования и подключения зависимостей. ADO.NET в .NET 8+ — Connection, Command, параметры и провайдеры; краткая история классического ADO (COM). ASP.NET - веб-платформа Microsoft для разработки серверных приложений, API и динамических сайтов. Экосистема .NET-приложений - поддерживаемые платформы, сценарии разработки и интеграция с современными устройствами. F# в экосистеме .NET - функциональный стиль, совместимость с платформой и применение в прикладной разработке.Платформа .NET
История платформы .NET
Архитектурные особенности .NET
Типы приложений на платформе .NET
Сборка и развёртывание .NET-приложений
Пакеты и зависимости в .NET
Инструменты разработки для .NET
NuGet - система управления пакетами
ADO.NET - доступ к данным
ASP.NET - веб-платформа Microsoft
Экосистема .NET-приложений
F# - функциональный язык в экосистеме .NET