Functional Programming in Kotlin: Exploring Monads and their Real-World Use Cases

Summit Kumar
9 min readMay 26, 2023

--

Monads are a powerful concept in functional programming that allows us to compose functions in a way that ensures safe and predictable behavior. They have become increasingly popular in recent years, particularly in languages like Kotlin that have strong support for functional programming.

In this blog, we will explore what monads are, how they are used in Kotlin functional programming, and some real-world use cases for monads.

What are Monads?
Monads are a design pattern that originated in category theory, a branch of mathematics that deals with the abstract study of structures and relationships between them. In functional programming, a monad is a way of encapsulating a sequence of computations and providing a consistent way of combining and sequencing them.

At a high level, a monad is a type constructor that provides two key functions: unit and bind. The unit function is responsible for wrapping a value of type T in a monad. The bind function is responsible for taking a monad that contains a value of type T and a function that takes a value of type T and returns a monad containing a value of type U, and then returns a monad containing a value of type U.

Here is an example of a simple monad in Kotlin:

class Maybe<T>(val value: T? = null) {
fun <U> bind(f: (T) -> Maybe<U>): Maybe<U> {
return if (value != null) f(value) else Maybe<U>()
}
}

fun <T> unit(value: T): Maybe<T> {
return Maybe(value)
}

This is a basic implementation of the Maybe monad represents a computation that may or may not return a value. The unit function wraps a value in a Maybe instance, while the bind function takes a function that returns a Maybe and applies it to the value in the current Maybe instance.

Monads in Kotlin
In Kotlin, monads are typically implemented using the flatMap function. The flatMap function is similar to the bind function in the example above, but it is a standard library function that is available on many types, including List, Sequence, and Optional.

Here is an example of using the flatMap function in Kotlin to work with a list of values:

val list = listOf(1, 2, 3)
val result = list.flatMap { x -> listOf(x, x * 2) }
println(result) // prints [1, 2, 2, 4, 3, 6]

In this example, the flatMap function is used to apply a function to each element of the list and then flatten the resulting list of lists into a single list. This is a common pattern in functional programming, and it is particularly useful when working with collections of data.

Real-World Use Cases for Monads

Optional/ Maybe Monad

The Optional or Maybe monad is used to handle nullable values and avoid null pointer exceptions. In Kotlin, you can use the java.util.Optional class or implement your version. Here's an example:

class Optional<T>(private val value: T?) {
fun <R> map(transform: (T) -> R): Optional<R> {
return if (value != null) Optional(transform(value)) else Optional(null)
}

fun <R> flatMap(transform: (T) -> Optional<R>): Optional<R> {
return if (value != null) transform(value) else Optional(null)
}
}

// Usage example
val name: Optional<String> = Optional("Sumit")
val upperCaseName: Optional<String> = name.map { it.toUpperCase() }

The code defines a custom Optional class that wraps a nullable value. It provides two main functions: map and flatMap. The map function applies a transformation to the wrapped value if it's not null, returning a new Optional with the transformed value. The flatMap function applies a transformation that returns another Optional and handles the case where the original value is null.

The usage example creates an Optional instance with a name and then uses map to transform the name to uppercase. If the name is null, the Optional will propagate the null value, avoiding null pointer exceptions.

The Optional/ Maybe monad allows handling nullable values in a structured and safe way. It provides a way to encapsulate the presence or absence of a value and allows you to perform computations on that value if it exists.

Result/Either Monad:

The Result or Either monad is used to represent computations that can either produce a successful result or an error. This is particularly useful for handling asynchronous operations, such as network requests or file I/O. Here’s an example using the Result type:

sealed class Result<out T> {
data class Success<out T>(val value: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()

fun <R> map(transform: (T) -> R): Result<R> {
return when (this) {
is Success -> Success(transform(value))
is Error -> this
}
}

fun <R> flatMap(transform: (T) -> Result<R>): Result<R> {
return when (this) {
is Success -> transform(value)
is Error -> this
}
}
}

// Usage example
fun getUserById(id: String): Result<User> {
// Perform network request or database query
return if (userExists(id)) Result.Success(User(id)) else Result.Error("User not found")
}

val userResult: Result<User> = getUserById("123")
val userNameResult: Result<String> = userResult.flatMap { Result.Success(it.name) }

The code defines a sealed class Result with two subclasses: Success and Error. The Success class holds a successful value, while the Error class holds an error message. The Result class provides map and flatMap functions that allow transforming the successful value while preserving error states.

The usage example demonstrates a function getUserById that performs a network request or a database query to fetch a user by ID. It returns an Result indicating either success with the user or an error if the user was not found. The flatMap function is then used to chain operations, extracting the user's name from the result.

The Result/ Either Monad allows you to handle success and error scenarios in a structured and composable way, without relying on exceptions or null values.

State Monad:

The State monad is used to encapsulate stateful computations. It allows you to pass and transform a state value along with the computation. Here’s a simplified example of the State monad:

data class State<S, out A>(val run: (S) -> Pair<A, S>) {
fun <B> flatMap(f: (A) -> State<S, B>): State<S, B> =
State { s0 ->
val (a, s1) = run(s0)
f(a).run(s1)
}

fun <B> map(f: (A) -> B): State<S, B> = flatMap { a -> State { s -> Pair(f(a), s) } }
}

// Usage example
data class CounterState(val count: Int)

fun incrementCounter(): State<CounterState, Unit> = State { state ->
Pair(Unit, state.copy(count = state.count + 1))
}

fun doubleCounter(): State<CounterState, Unit> = State { state ->
Pair(Unit, state.copy(count = state.count * 2))
}

val result: Pair<Unit, CounterState> = incrementCounter().flatMap { doubleCounter() }.run(CounterState(0))

The code defines a generic State class that represents a stateful computation. It takes two types of parameters: S for the state type and A for the computation result type. The State class provides flatMap and map functions that allow sequencing and transforming stateful computations.

The usage example demonstrates two stateful computations: incrementCounter and doubleCounter. Each function returns a State object that represents the transformation of the state. The flatMap function is used to chain the computations together, running incrementCounter first and then doubleCounter.

The run function is called on the final State instance, passing an initial state. It returns a Pair containing the result of the computations and the final state.

The State monad allows you to encapsulate stateful computations and sequence them together while maintaining and propagating the state in a controlled manner.

List Monad:

The List monad represents computations that operate on multiple values, allowing you to apply transformations to each value and combine the results. In Kotlin, you can use the standard List class or implement your version. Here's an example:

fun <T, R> List<T>.flatMap(transform: (T) -> List<R>): List<R> {
val result = mutableListOf<R>()
for (item in this) {
result.addAll(transform(item))
}
return result
}

// Usage example
val numbers = listOf(1, 2, 3, 4, 5)
val squaredNumbers = numbers.flatMap { number -> listOf(number * number) }

The code example demonstrates the List monad, which represents computations that operate on multiple values. In Kotlin, lists are already a suitable data structure for this purpose. The code provides an extension function flatMap that allows you to apply a transformation function to each element of a list and combine the results into a new list.

The usage example creates a list of numbers and applies a transformation to square each number. The flatMap function takes a lambda function that squares each number and returns it as a single-element list. The result is a new list with squared numbers.

The List monad allows you to perform computations on collections of values and handle the combination of results in a structured manner.

Reader Monad:

The Reader monad represents computations that depend on a shared environment or configuration. It allows you to pass the environment implicitly, making it available to functions without explicitly passing it as a parameter. Here’s a simplified implementation:

typealias Reader<E, A> = (E) -> A

fun <E, A, B> Reader<E, A>.flatMap(transform: (A) -> Reader<E, B>): Reader<E, B> =
{ env -> transform(this(env))(env) }

fun <E, A, B> Reader<E, A>.map(transform: (A) -> B): Reader<E, B> =
{ env -> transform(this(env)) }

// Usage example
data class AppConfig(val apiUrl: String)

fun fetchUser(userId: String): Reader<AppConfig, String> =
{ config -> "User $userId fetched from ${config.apiUrl}" }

fun logUser(user: String): Reader<AppConfig, Unit> =
{ config -> println("Logging user: $user to ${config.apiUrl}") }

val program: Reader<AppConfig, Unit> =
fetchUser("123").flatMap { user ->
logUser(user)
}

// Running the program
val config = AppConfig("https://api.example.com")
program(config)

The code example demonstrates the Reader monad, which represents computations that depend on a shared environment or configuration. It allows you to pass the environment implicitly, making it available to functions without explicitly passing it as a parameter.

The code defines a type alias Reader as a function that takes an environment (E) and returns a result (A). It provides two extension functions: flatMap and map. The flatMap function allows you to chain computations that depend on the environment, and the map function allows you to transform the result.

The usage example involves an AppConfig data class representing the application's configuration, and two functions: fetchUser and logUser. The fetchUser function takes a user ID and returns a Reader that depends on the AppConfig environment. The logUser function takes a user and returns a Reader that also depends on the AppConfig environment.

The program combines the fetchUser and logUser computations using flatMap, creating a chain of computations that implicitly pass the AppConfig environment.

The Reader monad allows you to encapsulate computations that depend on a shared environment and compose them while abstracting away the explicit passing of the environment.

Future/Promise Monad:

The Future or Promise monad represents asynchronous computations that produce a result in the future. It allows you to chain and compose operations on asynchronous values. In Kotlin, you can use libraries like kotlinx.coroutines to work with asynchronous computations. Here’s an example:

import kotlinx.coroutines.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

suspend fun <T> asyncOperation(): T = suspendCoroutine { continuation ->
// Simulating an async operation
GlobalScope.launch {
delay(1000) // Simulating a delay
val result = /* Perform some computation */
continuation.resume(result)
}
}

fun <T, R> Deferred<T>.flatMap(transform: (T) -> Deferred<R>): Deferred<R> =
GlobalScope.async { transform(await()).await() }

fun <T, R> Deferred<T>.map(transform: (T) -> R): Deferred<R> =
GlobalScope.async { transform(await()) }

// Usage example
val result: Deferred<Int> = asyncOperation<Int>().flatMap { value ->
asyncOperation<Int>().map { value + it }
}
runBlocking { println(result.await()) }

The code example demonstrates the Future or Promise monad, which represents computations that produce results asynchronously in the future. It allows you to chain and compose operations on these asynchronous values.

The code utilizes kotlinx.coroutines library to work with coroutines and asynchronous computations. The asyncOperation function simulates an asynchronous operation that returns a result in the future. It suspends the coroutine and resumes it when the result is ready.

The code provides two extension functions on Deferred objects: flatMap and map. The flatMap function allows you to chain asynchronous computations and the map function allows you to transform the result of an asynchronous computation.

The usage example creates a chain of two asyncOperation calls using flatMap and map. The second asyncOperation depends on the result of the first one. The await function is used to wait for the completion of the asynchronous computations and retrieve their results.

The Future/Promise monad allows you to work with asynchronous computations and chain them together in a structured and composable way, enabling you to handle the results once they are available.

Conclusion:
Monads are powerful constructs in functional programming that enable developers to handle complex scenarios with elegance and maintainability. In Kotlin, monads such as Option, Either, State, and Future provide practical solutions for null safety, error handling, stateful computations, and side-effect management. By leveraging monads, developers can write code that is more expressive, composable, and robust. By understanding and applying monads in Kotlin, developers can improve their code’s readability, maintainability, and error-handling capabilities.

--

--

Summit Kumar

Tech-savvy BTech IT professional with a passion for latest technologies. Always seeking new challenges & opportunities to expand my knowledge. #KeepLearning #IT