Императивные конструкции в F#
Императивный код в F# — когда уместен
F# — функциональный язык на платформе .NET: по умолчанию значения неизменяемы, ветвление — через match, обход коллекций — через map / filter / fold. При этом язык допускает привычные с C# или Java конструкции: while, for, изменяемые переменные, побочные эффекты (printfn, запись в файл).
Эта статья для тех, кто:
- портировал алгоритм с индексами и хочет увидеть синтаксис циклов в F#;
- читает Первую программу со
mutableи рекурсивным меню; - ищет
[<EntryPoint>]и понимание кода возврата изmain.
Задача — показать синтаксис, точку входа и критерии: где императивный стиль уместен, а где выразительнее рекурсия, Seq или агенты (189).
Неизменяемое и изменяемое — let и mutable
Обычная привязка:
let counter = 0
// counter <- 1 // ОШИБКА: counter не mutable
let создаёт неизменяемое значение. Повторное let counter = 1 в той же области — новая привязка, а не «присвоение в старую переменную».
Изменяемая ячейка:
let mutable counter = 0
counter <- counter + 1
| Элемент | Значение |
|---|---|
mutable | Разрешает перезапись |
<- | Оператор присваивания только для mutable |
counter + 1 | Считается новое значение, затем записывается в counter |
Когда счётчик или флаг — часть доменной модели, в F# чаще делают так:
- новая копия записи:
{ order with Status = Paid }; - свёртка:
List.foldнакапливает состояние безmutable; - MailboxProcessor (189) — одно место, где состояние меняется в ответ на сообщения.
mutable уместен в тонком слое: консольное меню-прототип, адаптер к StringBuilder, потоку .NET, UI-событию.
Циклы while
let readUntilEmpty () =
let mutable lines = []
let mutable line = System.Console.ReadLine()
while not (System.String.IsNullOrEmpty line) do
lines <- line :: lines
line <- System.Console.ReadLine()
List.rev lines
Разбор:
lines— изменяемый список строк; новые строки добавляют в начало (line :: lines), в концеList.revвосстанавливает порядок ввода.while условие do тело— тело выполняется, пока условие истинно.whileвозвращаетunit(()), не коллекцию. Результат собирают вручную вmutableили передают в аккумулятор.
Эквивалент в функциональном стиле — рекурсия с аккумулятором или чтение через Seq.unfold; while читается привычнее при переносе кода из C#.
Циклы for по диапазону
for i = 1 to 5 do
printfn "%d" i
1 to 5 — включительно с обеих сторон. Убывающий диапазон:
for i = 5 downto 1 do
printfn "%d" i
Подходит для классических алгоритмов с индексом 0 .. n-1 после портирования.
Циклы for .. in по коллекции
let xs = [1; 2; 3]
for x in xs do
printfn "%d" x
Перебор элементов без ручного индекса. Побочный эффект (печать) — нормальная цель; если нужен новый список, используют List.map:
let doubled = xs |> List.map (fun x -> x * 2)
| Задача | Идиоматичный приём |
|---|---|
| Преобразовать каждый элемент | List.map / Array.map |
| Оставить часть элементов | List.filter / Seq.filter |
| Свернуть в одно значение (сумма, макс) | List.fold / Seq.fold |
| Выполнить действие для каждого (лог, запись) | List.iter или for .. in |
| Число итераций заранее неизвестно | while или рекурсия |
Вложенные функции
let outer x =
let double y = y * 2
let inner z = double z + x
inner 10
// outer 1 ==> double 10 + 1 ==> 21
doubleвидна только внутриouter.innerиспользует иdouble, и параметрxвнешней функции.- Внешняя функция не видит локальные имена
double/inner.
Так заменяют «приватные» helper-методы без отдельного модуля.
Взаимная рекурсия and
let rec even n = if n = 0 then true else odd (n - 1)
and odd n = if n = 0 then false else even (n - 1)
even и odd ссылаются друг на друга; ключевое слово and связывает их в одной группе let.
В FSI повторный let с тем же именем в одной сессии создаёт новую привязку; в файле проекта область видимости задаётся структурой модуля.
Точка входа [<EntryPoint>]
Консольное приложение .NET должно иметь метод входа — с него начинается выполнение.
[<EntryPoint>]
let main argv =
printfn "Args: %A" argv
0
| Часть | Назначение |
|---|---|
[<EntryPoint>] | Атрибут: эта функция — вход в программу |
argv | Аргументы командной строки (string array) |
0 | Код завершения процесса (0 — успех; другие — ошибка для скриптов/CI) |
В шаблоне SDK иногда используют top-level код в Program.fs без явного main — компилятор генерирует точку входа сам. Для библиотек (classlib) атрибут не ставят: DLL не запускают напрямую.
В одной сборке ровно одна функция с [<EntryPoint>] — иначе ошибка компиляции.
Пример с аргументами:
dotnet run -- arg1 arg2
В main argv будет [|"arg1"; "arg2"|].
Композиция >> и <<, конвейер |>
let trim s = s.Trim()
let upper s = s.ToUpper()
let normalize = trim >> upper
// normalize " hi " ==> "HI"
let normalize2 s = s |> trim |> upper
| Запись | Порядок вычисления |
|---|---|
f >> g | Сначала f, результат передать в g (слева направо) |
g << f | То же направление данных, запись аргументов «справа налево» |
x |> f |> g | x подаётся в f, результат — в g |
normalize и normalize2 — один смысл; |> читается сверху вниз в длинных цепочках валидации.
Мост к функциональному стилю — тот же счётчик
Императивно (как в 182):
let mutable count = 0
let inc () = count <- count + 1
Через свёртку (без mutable в домене):
let steps = [ "+"; "+"; "-"; "+" ]
let count =
steps
|> List.fold (fun n op ->
match op with
| "+" -> n + 1
| "-" -> max 0 (n - 1)
| _ -> n) 0
fold проходит список «команд» и накапливает число. Так проще тестировать: вход — список, выход — число, без скрытого состояния.
Практическая рекомендация
- Бизнес-правила — чистые функции,
match,Result/Option. - Обход коллекций —
map/filter/fold, индекс — только если алгоритм того требует. - I/O и сеть —
task/async,useдляIDisposable(189). - Циклы и
mutable— граница с консолью, UI, legacy API .NET.
Дальше
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Платформа .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