Функциональное программирование в Swift: продвинутый уровень
Swift изначально проектировался как мультипарадигмальный язык, что делает его идеальной платформой для применения продвинутых техник функционального программирования. В этой статье мы рассмотрим концепции, которые выходят за рамки базовых замыканий и map/filter/reduce — речь пойдет о функторах, монадах, композиции функций, каррировании и мемоизации. Эти инструменты помогут вам писать более предсказуемый, тестируемый и выразительный код для iOS и macOS приложений.
Функторы и монады: работа с контекстами
Функтор — это тип, который реализует функцию map, позволяя применить трансформацию к значению внутри контекста. В Swift классическими примерами функторов являются Optional и Array.
// Функтор Optional
let value: Int? = 42
let result = value.map { $0 * 2 } // Optional(84)
// Функтор Array
let numbers = [1, 2, 3]
let doubled = numbers.map { $0 * 2 } // [2, 4, 6]
Монада — это функтор с дополнительной операцией flatMap (или >>= в Haskell). Она позволяет "разворачивать" вложенные контексты. Swift поддерживает монады через протокол Monad неявно, но ключевые типы (Optional, Result, Async) реализуют эту логику.
// Монада Optional: цепочка операций с возможным nil
func parseNumber(_ string: String) -> Int? { return Int(string) }
func half(_ number: Int) -> Int? { return number % 2 == 0 ? number / 2 : nil }
let input = "42"
let result = input.flatMap(parseNumber).flatMap(half) // Optional(21)
// Монада Result для обработки ошибок
enum AppError: Error { case invalidInput }
func safeDivide(_ a: Int, _ b: Int) -> Result<Int, AppError> {
guard b != 0 else { return .failure(.invalidInput) }
return .success(a / b)
}
let divisionResult = safeDivide(10, 2).flatMap { safeDivide($0, 1) } // .success(5)
Продвинутое применение: создание собственного монадического типа для работы с асинхронными операциями.
struct Async<T> {
let run: (@escaping (T) -> Void) -> Void
func map<U>(transform: @escaping (T) -> U) -> Async<U> {
return Async<U> { callback in
self.run { value in callback(transform(value)) }
}
}
func flatMap<U>(transform: @escaping (T) -> Async<U>) -> Async<U> {
return Async<U> { callback in
self.run { value in transform(value).run(callback) }
}
}
}
Композиция функций: строим pipeline
Композиция функций — это создание новой функции путем последовательного применения нескольких функций. В Swift это можно реализовать через оператор или замыкания.
// Оператор композиции
precedencegroup CompositionPrecedence {
associativity: left
higherThan: FunctionArrowPrecedence
}
infix operator >>> : CompositionPrecedence
func >>>(_ f: @escaping (A) -> B, _ g: @escaping (B) -> C) -> (A) -> C {
return { x in g(f(x)) }
}
// Пример: валидация и форматирование
let trimWhitespace: (String) -> String = { $0.trimmingCharacters(in: .whitespaces) }
let capitalize: (String) -> String = { $0.capitalized }
let addExclamation: (String) -> String = { $0 + "!" }
let processString = trimWhitespace >>> capitalize >>> addExclamation
print(processString(" hello world ")) // "Hello World!"
Композиция особенно полезна при работе с цепочками трансформаций данных, например, при обработке сетевых ответов.
// Продвинутая композиция с обработкой ошибок
typealias ResultFunction = (T) -> Result<U, Error>
func compose(_ f: @escaping ResultFunction<T, U>, _ g: @escaping ResultFunction<U, V>) -> ResultFunction<T, V> {
return { input in
f(input).flatMap(g)
}
}
let fetchUser: (Int) -> Result<User, Error> = { id in /* ... */ }
let validateUser: (User) -> Result<User, Error> = { user in /* ... */ }
let processUser = compose(fetchUser, validateUser)
Каррирование: частичное применение функций
Каррирование — это преобразование функции, принимающей несколько аргументов, в последовательность функций, каждая из которых принимает один аргумент. В Swift это можно реализовать вручную или с помощью вспомогательных утилит.
// Каррированная функция
func curriedAdd(_ a: Int) -> (Int) -> Int {
return { b in a + b }
}
let addFive = curriedAdd(5)
print(addFive(3)) // 8
// Каррирование для конфигурации
func configureLogger(level: LogLevel) -> (String) -> Void {
return { message in
print("[\(level.rawValue)] \(message)")
}
}
let errorLogger = configureLogger(level: .error)
let infoLogger = configureLogger(level: .info)
errorLogger("File not found") // [ERROR] File not found
infoLogger("User logged in") // [INFO] User logged in
Продвинутый пример: каррирование для создания гибких UI-компонентов.
// Каррирование для стилизации UI
func styleLabel(font: UIFont, color: UIColor, alignment: NSTextAlignment) -> (UILabel) -> Void {
return { label in
label.font = font
label.textColor = color
label.textAlignment = alignment
}
}
let titleStyle = styleLabel(font: .boldSystemFont(ofSize: 18), color: .black, alignment: .center)
let subtitleStyle = styleLabel(font: .systemFont(ofSize: 14), color: .gray, alignment: .left)
let titleLabel = UILabel()
titleStyle(titleLabel) // Применяем стиль
Мемоизация: оптимизация вычислений
Мемоизация — это техника кэширования результатов вызова функций для ускорения повторных вычислений с теми же аргументами. Особенно эффективна для чистых функций.
// Общая функция мемоизации
func memoize<In: Hashable, Out>(_ function: @escaping (In) -> Out) -> (In) -> Out {
var cache = [In: Out]()
let queue = DispatchQueue(label: "memoize.queue", attributes: .concurrent)
return { input in