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

Сопоставление с образцом в 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

  1. Скопируйте type Shape и area в dotnet fsi.
  2. Вызовите area (Circle 2.0) и area (Rectangle (3.0, 4.0)).
  3. Добавьте вариант Triangle — прочитайте предупреждение компилятора.
  4. Перепишите parsePositive и вызовите с "10", "0", "abc".

Дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).