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

Императивные конструкции в 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 |> gx подаётся в 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 проходит список «команд» и накапливает число. Так проще тестировать: вход — список, выход — число, без скрытого состояния.


Практическая рекомендация

  1. Бизнес-правила — чистые функции, match, Result / Option.
  2. Обход коллекцийmap / filter / fold, индекс — только если алгоритм того требует.
  3. I/O и сетьtask / async, use для IDisposable (189).
  4. Циклы и mutable — граница с консолью, UI, legacy API .NET.

Дальше


См. также

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