1. Ментальная модель пайплайна: «много → много → много»
Когда мы впервые видим array.filter(...).map(...).filter(...).map(...), это ощущается как магия: «ого, один выразительный выраженец вместо трёх циклов и четырёх временных переменных». И да, это один из суперсиловых приёмов Swift: писать преобразования данных компактно и довольно безопасно.
Но у любой суперсилы есть слабость. Цепочки легко обрастают условиями, нормализацией строк, if/else, guard, попытками «чуть-чуть подлогировать» через print, и внезапно у вас не код, а новогодняя гирлянда на 40 лампочек: вроде красиво, но если одна перегорит — ищи потом, какая именно.
Отдельная (занудная, но полезная) причина: длинные цепочки на lazy-коллекциях могут порождать очень «страшные» типы и неприятные сообщения компилятора. В Swift сообществе это прямо называют проблемой: “large, painful type names” при цепочках map/filter на ленивых коллекциях. Мы lazy сегодня не изучаем, но вывод для стиля всё равно важный: чем проще и понятнее шаги, тем меньше сюрпризов.
Чтобы писать цепочки читабельно, полезно держать в голове простую модель. filter, map, compactMap — это про преобразование коллекции в другую коллекцию (то есть «много элементов на входе, много на выходе»). filter уменьшает количество элементов, map меняет каждый элемент, compactMap меняет и одновременно выкидывает nil.
Эта модель ещё важна потому, что в цепочке лучше видеть «ступеньки»: сначала отобрали нужное, потом преобразовали, потом снова отобрали… и так далее. Когда вы видите такую цепочку, вы должны уметь прочитать её вслух сверху вниз как рецепт. Если вслух получается бред — код тоже, скорее всего, бред (просто компилятор слишком вежливый и молчит).
Небольшая табличка, чтобы закрепить «кто что делает»:
| Метод | Что делает с элементами | Ключевой вопрос |
|---|---|---|
|
оставляет часть элементов | «Этот элемент подходит?» |
|
превращает каждый элемент в новый | «Во что преобразовать элемент?» |
|
превращает, но может выкинуть элемент | «Могу ли я получить значение? Если нет — выбросить» |
2. Мини-проект: «карманная библиотека»
Чтобы примеры не висели в воздухе, давайте продолжать наш учебный мини-проект: маленькая «библиотека» книг в памяти. Мы пока не дошли до struct, поэтому используем tuples — легковесно и по делу.
Начнём с данных:
import Foundation
let books: [(id: Int, title: String, year: Int)] = [
(1, "Swift для тех, кто любит чай", 2021),
(2, "Алгоритмы без боли", 2019),
(3, "Паттерны проектирования на салфетке", 2004),
(4, "Unicode: месть смайликов", 2020)
]
print(books.count) // 4
Наша цель на лекцию: научиться получать из этого массива разные «представления» (списки названий, списки id, отфильтрованные результаты) так, чтобы код оставался читаемым.
4. Стиль оформления цепочек: переносы и «ступеньки»
Первое, что почти всегда улучшает читаемость: не писать цепочку в одну строку, даже если она помещается. В одну строку хорошо помещаются только шутки и то не всегда. Цепочки лучше показывать как вертикальный пайплайн.
Сравним.
Вот так — технически нормально, но глаз быстро устаёт:
import Foundation
let recentTitles = books.filter { $0.year >= 2020 }.map { $0.title }
print(recentTitles) // ["Swift для тех, кто любит чай", "Unicode: месть смайликов"]
А вот так — обычно читается легче, потому что каждый шаг — отдельная строка:
import Foundation
let recentTitles = books
.filter { $0.year >= 2020 }
.map { $0.title }
print(recentTitles) // ["Swift для тех, кто любит чай", "Unicode: месть смайликов"]
Заметьте важную вещь: мы не стали «умнее», мы стали понятнее. Это главный стиль в функциональных цепочках: показывать шаги как шаги.
5. Порядок шагов: чаще filter раньше map
Есть простое правило, которое полезно и для читаемости, и для здравого смысла: если вы можете отфильтровать раньше — фильтруйте раньше. Тогда дальше по цепочке «течёт» меньше элементов, и вы не делаете лишней работы.
Но ещё важнее: так цепочка читается как «сначала выбрали, потом преобразовали». В бытовой логике это почти всегда естественнее.
Например, нам нужны названия книг, в которых есть слово "Swift", и мы хотим привести названия к нижнему регистру:
import Foundation
let swiftTitlesLowercased = books
.filter { $0.title.contains("Swift") }
.map { $0.title.lowercased() }
print(swiftTitlesLowercased) // ["swift для тех, кто любит чай"]
Если сделать наоборот (сначала map, потом filter), код всё ещё будет работать, но мысль станет менее прямой: «сначала преобразуем всё, потом выберем часть».
6. Когда цепочка становится «простынёй»
Обычно момент, когда цепочка перестаёт быть «рецептом» и превращается в «квест», заметен по нескольким симптомам.
Три признака, что пора остановиться
Первый симптом — внутри map или filter появляется замыкание на 10–15 строк. Формально это всё ещё «один шаг», но по факту вы засунули туда мини-программу.
Второй симптом — внутри шага появляется вложенный if/else, а ещё хуже — несколько if/else подряд. Это почти всегда признак, что вы смешали несколько смысловых шагов в один.
Третий симптом — вы ловите себя на мысли «я сейчас добавлю print, чтобы понять, что происходит». В этот момент у вас либо баг, либо цепочка стала слишком сложной для головы, и её пора развернуть.
Антипример: компилируется, но читать тяжело
Покажу типичный антипример «простыня». Он компилируется, но читать его тяжело:
import Foundation
let titles = books
.filter { book in
let t = book.title.lowercased()
if t.contains("swift") { return true }
if t.contains("алгоритм") { return true }
return false
}
.map { book in
"\(book.title) (\(book.year))"
}
print(titles) // ["Swift для тех, кто любит чай (2021)", "Алгоритмы без боли (2019)"]
Да, оно работает. Но уже хочется спросить: «а можно так, чтобы без расследования?»
7. Приёмы против «простыни»
Тут важная мысль: это не «проигрыш функциональности». Это «выигрыш мозгов». Мы не обязаны держать всё в одной цепочке, если из-за этого теряется смысл.
Разделяйте шаги промежуточными let
Самый доступный способ сделать цепочку читабельной — разложить её на несколько промежуточных let.
Перепишем предыдущий пример так, чтобы каждый шаг имел имя:
import Foundation
let lowered = books.map { (id: $0.id, title: $0.title.lowercased(), year: $0.year) }
let filtered = lowered.filter { book in
book.title.contains("swift") || book.title.contains("алгоритм")
}
let formatted = filtered.map { book in
"\(book.title) (\(book.year))"
}
print(formatted) // ["swift для тех, кто любит чай (2021)", "алгоритмы без боли (2019)"]
Плюс такого подхода: вы можете поставить print(filtered) и понять, что происходит, не внедряя «отладку» внутрь filter.
Минус: кода стало больше. Но это тот случай, когда «больше строк» не значит «хуже». Важно, чтобы строки были честными и говорящими.
Используйте compactMap, когда логика «получилось — берём, нет — пропускаем»
Иногда «правильная» цепочка получается из трёх шагов: преобразовали в Optional, отфильтровали nil, развернули Optional. Практически: если вы видите конструкцию «если не получилось — пропусти», compactMap часто выглядит честнее.
Допустим, мы хотим получить id книг, год которых можно «прочитать» из строки (сценарий притянут, но показывает механику). Пусть у нас есть массив строк, а мы хотим вытащить корректные числа:
import Foundation
let rawYears = ["2021", "oops", "2019", ""]
let years = rawYears.compactMap { Int($0) }
print(years) // [2021, 2019]
Тут compactMap очень читабелен: «попробуй превратить, не получилось — выкинь».
И важная мысль про стиль: иногда лучше сделать один compactMap со спокойным guard, чем собирать длинную гирлянду из map/filter/map.
Вынесите правила из цепочки в именованные замыкания
Мы ещё не дошли до лекции, где будем активно передавать функции как значения, но уже можем вынести замыкание в константу, чтобы цепочка читалась как набор именованных правил.
Например, хотим отбирать «современные книги» и форматировать строку для вывода:
import Foundation
let isModern: ((id: Int, title: String, year: Int)) -> Bool = { book in
book.year >= 2020
}
let formatLine: ((id: Int, title: String, year: Int)) -> String = { book in
"\(book.title) — \(book.year)"
}
let modernLines = books
.filter(isModern)
.map(formatLine)
print(modernLines) // ["Swift для тех, кто любит чай — 2021", "Unicode: месть смайликов — 2020"]
Цепочка стала похожа на предложение: «отфильтруй modern → преобразуй в line».
Следите за типами: что именно «течёт» по цепочке
Цепочка становится нечитаемой ещё и тогда, когда вы перестаёте понимать, какой тип на каждом шаге. Особенно часто это происходит при compactMap, потому что он убирает Optional.
Покажу на коротком примере: берём список названий, пытаемся найти год в конце строки, и берём только те, где год удалось вытащить.
import Foundation
let lines = ["Swift 2021", "Algo ???", "Unicode 2020"]
let years = lines.compactMap { line -> Int? in
let parts = line.split(separator: " ")
guard let last = parts.last else { return nil }
return Int(last)
}
print(years) // [2021, 2020]
Здесь стиль важен в мелочах: я явно написал -> Int?, чтобы было понятно, что compactMap ожидает Optional, и я не «теряю мысль» в середине замыкания.
8. Пример: поиск по запросу как пайплайн
Теперь соберём более жизненный сценарий: пользователь вводит строку запроса, а мы хотим получить список книг, где запрос встречается в названии, без учета регистра и с игнорированием лишних пробелов.
Сначала сделаем простую функцию нормализации запроса и названия. Пока без CharacterSet-фильтрации (это глубже и нам сейчас не нужно), ограничимся trimmingCharacters(in:) и lowercased().
import Foundation
func normalize(_ s: String) -> String {
s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
let query = " sWiFt "
let normalizedQuery = normalize(query)
print(normalizedQuery) // swift
Теперь — сам пайплайн поиска:
import Foundation
let queryNorm = normalize(" sWiFt ")
let matches = books
.filter { normalize($0.title).contains(queryNorm) }
.map { $0.title }
print(matches) // ["Swift для тех, кто любит чай"]
Это уже неплохо, но есть «скрытая» проблема: мы нормализуем title для каждой книги прямо внутри filter. Для маленького массива это не страшно. Но для стиля и ясности иногда лучше сделать нормализацию отдельным шагом, чтобы filter был максимально «логическим».
Развернём в два шага — и получим код, который легче объяснить новичку:
import Foundation
let queryNorm = normalize(" sWiFt ")
let prepared = books.map { book in
(id: book.id, titleNorm: normalize(book.title), titleRaw: book.title)
}
let titles = prepared
.filter { $0.titleNorm.contains(queryNorm) }
.map { $0.titleRaw }
print(titles) // ["Swift для тех, кто любит чай"]
Да, чуть больше строк. Зато теперь пайплайн буквально такой: «подготовь данные → фильтруй по нормализованному → верни оригинальные названия».
Можно даже представить это как блок-схему:
flowchart TD
A["books: [(id, title, year)]"] --> B["map: подготовка titleNorm"]
B --> C["filter: titleNorm contains queryNorm"]
C --> D["map: titleRaw"]
D --> E["[String] результатов"]
9. Когда лучше обычный for-in
Иногда лучший стиль — это не цепочка. Это нормально. Swift не выдаёт медаль за «самый функциональный код месяца» (а если бы выдавал — она бы была Optional, и вы бы не знали, получите ли вы её вообще).
Если у вас сложная логика, несколько условий, нужно собирать разные результаты (например, отдельно ошибки, отдельно успешные элементы), или нужно остановиться на первом совпадении — обычный for-in часто будет честнее и понятнее.
Например, хотим найти первую книгу, в названии которой есть “unicode” (без учёта регистра). Да, можно сделать first(where:), но он пока у нас не был центральной темой. Покажу через цикл как «читабельный референс»:
import Foundation
let target = "unicode"
var foundTitle: String? = nil
for book in books {
if book.title.lowercased().contains(target) {
foundTitle = book.title
break
}
}
print(foundTitle ?? "Not found") // Unicode: месть смайликов
Смысл раздела: цепочки — это инструмент. Когда инструмент начинает мешать мысли — инструмент меняют.
10. Типичные ошибки при работе с цепочками map/filter
Ошибка №1: цепочка из 5–7 шагов в одну строку.
Такой код может выглядеть «компактно», но он не компактен для мозга. Если цепочка длиннее двух-трёх шагов, почти всегда полезно поставить переносы строк и сделать «ступеньки». Это не косметика, это способ показать структуру.
Ошибка №2: замыкание на 15 строк внутри filter или map.
Когда правило отбора или преобразования занимает полэкрана, вы фактически спрятали ещё одну программу внутри шага. Чаще всего это сигнал вынести часть логики в отдельный шаг (промежуточный let) или переписать на compactMap с guard, если логика «либо получилось, либо выкинули».
Ошибка №3: смешивать преобразование и побочные эффекты.
print внутри filter или map может быть полезен для отладки, но он резко ухудшает чтение пайплайна: вы уже не уверены, это «логика данных» или «логика вывода». Если хочется логировать — временно разверните пайплайн на шаги с промежуточными переменными и печатайте их отдельно.
Ошибка №4: потерять типы по дороге.
Особенно часто новички путаются в compactMap: «почему после map у меня [Int?], а после compactMap уже [Int]?» Старайтесь явно прописывать возвращаемый тип замыкания (-> Int?) в местах, где это помогает, и давайте переменным говорящие имена (rawYears, parsedYears).
Ошибка №5: делать цепочку ради цепочки.
Если код в цепочке читается хуже, чем простой for-in, значит, вы выбрали не тот инструмент. Это не поражение, это взрослая инженерная привычка: выбирать самое понятное решение, а не самое «модное».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ