Функциональное программирование в Swift: от замыканий до композиции
Swift изначально проектировался как мультипарадигмальный язык. Он поддерживает объектно-ориентированное, протокол-ориентированное и функциональное программирование. Функциональный подход помогает писать более предсказуемый, тестируемый и параллельно-безопасный код. В этой статье мы разберём ключевые концепции функционального программирования на Swift с практическими примерами.
1. Чистые функции и иммутабельность
Основа функционального стиля — чистые функции. Такая функция всегда возвращает один и тот же результат для одних и тех же аргументов и не имеет побочных эффектов (не меняет внешнее состояние).
// Нечистая функция — изменяет внешнюю переменную
var total = 0
func addToTotal(_ value: Int) -> Int {
total += value
return total
}
// Чистая функция — только вычисление
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
Иммутабельность (неизменяемость) достигается через let и структуры с семантикой значения. Вместо изменения массива мы создаём новый:
let numbers = [1, 2, 3, 4, 5]
// Вместо var и append:
let newNumbers = numbers + [6] // [1, 2, 3, 4, 5, 6]
2. Замыкания (Closures) как строительные блоки
Замыкания в Swift — это анонимные функции, которые могут захватывать переменные из окружающего контекста. Они являются «гражданами первого класса»: их можно передавать как параметры и возвращать из функций.
// Простое замыкание
let multiply: (Int, Int) -> Int = { $0 * $1 }
let result = multiply(3, 4) // 12
// Замыкание с захватом контекста
var counter = 0
let increment = { counter += 1 }
increment() // counter = 1
increment() // counter = 2
Замыкания активно используются в методах высшего порядка, таких как sorted:
let names = ["Иван", "Анна", "Борис", "Елена"]
let sorted = names.sorted { $0.localizedStandardCompare($1) == .orderedAscending }
print(sorted) // ["Анна", "Борис", "Елена", "Иван"]
3. Методы высшего порядка: map, filter, reduce
Это «рабочая лошадка» функционального стиля в Swift. Они позволяют обрабатывать коллекции без явных циклов и мутации.
map
Преобразует каждый элемент коллекции:
let prices = [100, 200, 300]
let withTax = prices.map { $0 * 1.2 }
print(withTax) // [120.0, 240.0, 360.0]
// Реальный пример: извлечение имён из массива моделей
struct User {
let name: String
let age: Int
}
let users = [User(name: "Алексей", age: 30), User(name: "Мария", age: 25)]
let userNames = users.map { $0.name }
print(userNames) // ["Алексей", "Мария"]
filter
Отбирает элементы по условию:
let numbers = [1, 2, 3, 4, 5, 6]
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers) // [2, 4, 6]
// Фильтрация пользователей старше 18 лет
let adults = users.filter { $0.age >= 18 }
reduce
Сводит коллекцию к одному значению:
let sum = numbers.reduce(0, +) // 21
// Собрать строку из массива
let words = ["Функциональное", "программирование", "Swift"]
let sentence = words.reduce("") { $0.isEmpty ? $1 : $0 + " " + $1 }
print(sentence) // "Функциональное программирование Swift"
Комбинирование методов
Самое интересное — цепочки вызовов:
let transactions = [250, -50, 100, -200, 300, -100]
let balance = transactions
.filter { $0 > 0 } // только доходы
.map { $0 * 0.87 } // вычитаем налог 13%
.reduce(0, +) // суммируем
print(balance) // 565.5
4. Композиция функций и каррирование
Композиция позволяет объединять несколько функций в одну. В Swift нет встроенного оператора композиции, но его легко реализовать:
precedencegroup CompositionPrecedence {
associativity: left
higherThan: AssignmentPrecedence
}
infix operator >>>: CompositionPrecedence
func >>><A, B, C>(left: @escaping (A) -> B, right: @escaping (B) -> C) -> (A) -> C {
return { right(left($0)) }
}
// Использование
let addTax = { (price: Double) in price * 1.2 }
let formatPrice = { (price: Double) in "\(price) руб." }
let displayPrice = addTax >>> formatPrice
print(displayPrice(100)) // "120.0 руб."
Каррирование — преобразование функции с несколькими аргументами в цепочку функций с одним аргументом:
// Обычная функция
func greet(_ greeting: String, to name: String) -> String {
return "\(greeting), \(name)!"
}
// Каррированная версия
func curriedGreet(_ greeting: String) -> (String) -> String {
return { name in "\(greeting), \(name)!" }
}
let helloGreet = curriedGreet("Привет")
print(helloGreet("Анна")) // "Привет, Анна!"
print(helloGreet("Иван")) // "Привет, Иван!"
5. Обработка опциональных значений (Optionals)
Функциональный подход отлично работает с Optional. Вместо вложенных if let используйте map и flatMap:
let optionalNumber: Int? = 5
let doubled = optionalNumber.map { $0 * 2 } // Optional(10)
// flatMap для вложенных опционалов
let stringDict: [String: String?] = ["key1": "123", "key2": nil]
let value = stringDict["key1"]?.flatMap { Int($0) }