Programmation fonctionnelle en Swift : niveau avancé
Swift a été conçu dès le départ comme un langage multi-paradigme, ce qui en fait une plateforme idéale pour appliquer des techniques avancées de programmation fonctionnelle. Dans cet article, nous explorerons des concepts qui vont au-delà des fermetures de base et de map/filter/reduce — nous parlerons des foncteurs, des monades, de la composition de fonctions, du curryfication et de la mémorisation. Ces outils vous aideront à écrire un code plus prévisible, testable et expressif pour les applications iOS et macOS.
Foncteurs et monades : travailler avec des contextes
Un foncteur est un type qui implémente la fonction map, permettant d'appliquer une transformation à une valeur à l'intérieur d'un contexte. En Swift, les exemples classiques de foncteurs sont Optional et Array.
// Foncteur Optionallet value: Int? = 42let result = value.map { $0 * 2 } // Optional(84)
// Foncteur Arraylet numbers = [1, 2, 3]let doubled = numbers.map { $0 * 2 } // [2, 4, 6]Une monade est un foncteur avec une opération supplémentaire flatMap (ou >>= en Haskell). Elle permet de "déballer" des contextes imbriqués. Swift prend en charge les monades via le protocole Monad de manière implicite, mais les types clés (Optional, Result, Async) implémentent cette logique.
// Monade Optional : chaîne d'opérations avec nil possiblefunc 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)
// Monade Result pour la gestion des erreursenum 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)Application avancée : création d'un type monadique personnalisé pour travailler avec des opérations asynchrones.
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) } } }}Composition de fonctions : construire un pipeline
La composition de fonctions consiste à créer une nouvelle fonction en appliquant séquentiellement plusieurs fonctions. En Swift, cela peut être implémenté via un opérateur ou des fermetures.
// Opérateur de compositionprecedencegroup 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)) }}
// Exemple : validation et formatagelet trimWhitespace: (String) -> String = { $0.trimmingCharacters(in: .whitespaces) }let capitalize: (String) -> String = { $0.capitalized }let addExclamation: (String) -> String = { $0 + "!" }
let processString = trimWhitespace >>> capitalize >>> addExclamationprint(processString(" hello world ")) // "Hello World!"La composition est particulièrement utile pour travailler avec des chaînes de transformations de données, par exemple lors du traitement de réponses réseau.
// Composition avancée avec gestion des erreurstypealias ResultFunction = (T) -> Result<U, Error>
func compose(_ f: @escaping ResultFunction<T, U>, _ g: @escaping ResultFunction<U, V>) -> ResultFunction<T, V> { return { input in