Functional Programming in Kotlin: Advanced Level
Kotlin is a multi-paradigm language that seamlessly combines object-oriented and functional approaches. If you have already mastered basic functional programming (FP) concepts such as map, filter, fold, and lambdas, it's time to dive into advanced techniques. In this article, we will explore function composition, currying, lazy evaluation, and also get acquainted with functors and monads using Kotlin examples. All code examples can be run in your IDE.
1. Function Composition and Currying
Composition is creating a new function by sequentially applying two or more functions. Kotlin does not have a built-in composition operator like Haskell, but it can be easily implemented via extension functions.
Function Composition Example
// Define functionsval double: (Int) -> Int = { it * 2 }val increment: (Int) -> Int = { it + 1 }
// Composition: first double, then 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
Currying is transforming a function of multiple arguments into a chain of functions, each taking one argument. In Kotlin, this is implemented using nested lambdas.
// Regular functionfun sum(a: Int, b: Int): Int = a + b
// Curried versionfun curriedSum(a: Int): (Int) -> Int = { b -> a + b }
val add5 = curriedSum(5)println(add5(3)) // 8
// Universal function for curryingfun <A, B, C> ((A, B) -> C).curry(): (A) -> (B) -> C = { a -> { b -> this(a, b) } }
val curriedSum2 = ::sum.curry()println(curriedSum2(10)(20)) // 30Currying is especially useful when working with partial application and function factories.
2. Functors and Monads: Theory in Practice
A functor is a container that can be mapped using a function. A monad is a functor with the ability to compose containers (flatMap). In Kotlin, a prominent example of a monad is Result (or Either), as well as Sequence.
Example with Result (Either monad)
data class User(val name: String, val email: String)
fun parseUser(json: String): Result<User> = runCatching { // Simulating parsing 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")}
// Monadic chainval result = parseUser("{...}") .flatMap { user -> sendEmail(user) } .map { message -> "Success: $message" } .onFailure { error -> println("Error: ${error.message}") }
println(result.getOrDefault("Failed"))Here, flatMap allows avoiding nested success/error checks, making the code linear and readable.
Creating Your Own Functor: Option Type
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)