Programmation fonctionnelle en Kotlin : niveau avancé
Kotlin est un langage multi-paradigme qui combine harmonieusement les approches orientée objet et fonctionnelle. Si vous maîtrisez déjà les concepts de base de la programmation fonctionnelle (PF), comme map, filter, fold et les lambdas, il est temps de plonger dans les techniques avancées. Dans cet article, nous aborderons la composition de fonctions, le currying, l'évaluation paresseuse, ainsi que les foncteurs et les monades avec Kotlin. Tous les exemples de code peuvent être exécutés dans votre IDE.
1. Composition de fonctions et currying
La composition consiste à créer une nouvelle fonction en appliquant successivement deux ou plusieurs fonctions. Kotlin n'a pas d'opérateur de composition intégré comme Haskell, mais il peut être facilement implémenté via des fonctions d'extension.
Exemple de composition de fonctions
// Définir les fonctionsval double: (Int) -> Int = { it * 2 }val increment: (Int) -> Int = { it + 1 }
// Composition : d'abord double, puis incrementinfix fun <A, B, C> ((B) -> C).compose(f: (A) -> B): (A) -> C = { a -> this(f(a)) }
val doubleThenIncrement = increment compose doubleprintln(doubleThenIncrement(5)) // 11 (5*2 + 1)Currying
Le currying est la transformation d'une fonction à plusieurs arguments en une chaîne de fonctions, chacune prenant un seul argument. En Kotlin, cela se fait avec des lambdas imbriquées.
// Fonction normalefun sum(a: Int, b: Int): Int = a + b
// Version curryfiéefun curriedSum(a: Int): (Int) -> Int = { b -> a + b }
val add5 = curriedSum(5)println(add5(3)) // 8
// Fonction universelle pour le curryingfun <A, B, C> ((A, B) -> C).curry(): (A) -> (B) -> C = { a -> { b -> this(a, b) } }
val curriedSum2 = ::sum.curry()println(curriedSum2(10)(20)) // 30Le currying est particulièrement utile pour l'application partielle et les fabriques de fonctions.
2. Foncteurs et monades : théorie en pratique
Un foncteur est un conteneur qui peut être transformé (map) à l'aide d'une fonction. Une monade est un foncteur avec la capacité de composer des conteneurs (flatMap). En Kotlin, des exemples frappants de monade sont Result (ou Either), ainsi que Sequence.
Exemple avec Result (monade Either)
data class User(val name: String, val email: String)
fun parseUser(json: String): Result<User> = runCatching { // Simulation d'analyse if (json.isBlank()) throw IllegalArgumentException("Empty JSON") User("Alice", "alice@example.com")}
fun sendEmail(user: User): Result<String> = runCatching { if (user.email.contains("@")) "Email sent to ${user.email}" else throw IllegalArgumentException("Invalid email")}
// Chaîne monadiqueval result = parseUser("{...}") .flatMap { user -> sendEmail(user) } .map { message -> "Success: $message" } .onFailure { error -> println("Error: ${error.message}") }
println(result.getOrDefault("Failed"))Ici, flatMap permet d'éviter les vérifications imbriquées de succès/échec, rendant le code linéaire et lisible.
Créer son propre foncteur : le type Option
sealed class Option<out T> { data class Some<T>(val value: T) : Option<T>() object None : Option<Nothing>()}
fun <T, R> Option<T>.map(f: (T) -> R): Option<R> = when (this) { is Option.Some -> Option.Some(f(value)) is Option.None -> Option.None}
fun <T, R> Option<T>.flatMap(f: (T) -> Option<R>): Option<R> = when (this) { is Option.Some -> f(value) is Option.None -> Option.None}
val maybeNumber: Option<Int> = Option.Some(42)val resultOption = maybeNumber .map { it * 2 } .flatMap { Option.Some("Value: $it") }
println(resultOption) // Some(value=Value: 84)