Kotlin functional programming patterns

In Kotlin functional programming patterns leverages higher-order functions, immutability, and concise syntax to write clean, maintainable, and robust code. This guide provides an overview of key functional programming patterns in Kotlin, focusing on mapping and transformation, reducing and folding, laziness and sequences, and pattern matching with sealed classes, accompanied by real-world examples.

1. Mapping and Transformation

Mapping and transformation are core concepts in functional programming. They allow you to apply a function to elements in a collection, transforming them in various ways.

1.1. map

Transforms each element of a collection by applying a function.

Example: Transforming a list of prices with a discount

Kotlin
val prices = listOf(100.0, 200.0, 300.0)
val discountedPrices = prices.map { it * 0.9 }

fun main() {
    println(discountedPrices)  // Output: [90.0, 180.0, 270.0]
}

1.2 flatMap

Transforms each element into a collection and flattens the result.

Example: Splitting a list of sentences into words

Kotlin
val sentences = listOf("Hello world", "Kotlin is awesome")
val words = sentences.flatMap { it.split(" ") }

fun main() {
    println(words)  // Output: [Hello, world, Kotlin, is, awesome]
}

1.3 filter

Selects elements based on a predicate.

Example: Filtering a list of users to find those who are active

Kotlin
data class User(val name: String, val isActive: Boolean)

val users = listOf(
    User("Alice", true),
    User("Bob", false),
    User("Charlie", true)
)
val activeUsers = users.filter { it.isActive }

fun main() {
    println(activeUsers)  // Output: [User(name=Alice, isActive=true), User(name=Charlie, isActive=true)]
}

1.4 Using mapNotNull

Applies a function and removes null results.

Example: Transforming a list of nullable strings to uppercase and removing nulls

Kotlin
val names = listOf("Alice", null, "Bob")
val nonNullNames = names.mapNotNull { it?.toUpperCase() }

fun main() {
    println(nonNullNames)  // Output: [ALICE, BOB]
}

1.5 filterNotNull

Filters out null values from a collection.

Example: Filtering out null values from a list of strings

Kotlin
val names = listOf("Alice", null, "Bob")
val validNames = names.filterNotNull()

fun main() {
    println(validNames)  // Output: [Alice, Bob]
}

2. Reducing and Folding

Reducing and folding are used to aggregate or combine elements of a collection into a single result.

2.1. fold

Accumulates a value starting from an initial value and applying an operation from left to right.

Example: Calculating the total revenue from a list of daily revenues

Kotlin
val dailyRevenues = listOf(100.0, 200.0, 150.0)
val totalRevenue = dailyRevenues.fold(0.0) { acc, revenue -> acc + revenue }

fun main() {
    println(totalRevenue)  // Output: 450.0
}

2.2 reduce

Similar to fold but uses the first element as the initial value.

Example: Finding the maximum value in a list of numbers

Kotlin
val numbers = listOf(3, 5, 2, 8, 6)
val maxNumber = numbers.reduce { acc, number -> if (number > acc) number else acc }

fun main() {
    println(maxNumber)  // Output: 8
}

2.2. Using foldRight

Similar to fold, but operates from right to left.

Example: Creating a comma-separated string from a list of words

Kotlin
val words = listOf("Kotlin", "is", "awesome")
val sentence = words.foldRight("") { word, acc -> if (acc.isEmpty()) word else "$word, $acc" }

fun main() {
    println(sentence)  // Output: Kotlin, is, awesome
}

2.3 reduceRight

Similar to reduce, but operates from right to left.

Example: Finding the sum of numbers from right to left

Kotlin
val numbers = listOf(1, 2, 3, 4)
val sumRight = numbers.reduceRight { number, acc -> number + acc }

fun main() {
    println(sumRight)  // Output: 10
}

3. Laziness and Sequences

Laziness allows you to defer computation until it’s needed, which can improve performance when working with large datasets.

3.1. Generating Sequences

Sequences provide a way to work with potentially infinite collections using lazy evaluation.

Example: Generating an infinite sequence of natural numbers

Kotlin
val naturalNumbers = generateSequence(1) { it + 1 }

fun main() {
    println(naturalNumbers.take(5).toList())  // Output: [1, 2, 3, 4, 5]
}

3.2. Using Sequence Builders

Sequence builders allow you to create sequences using yield to produce values.

Example: Generating a sequence of Fibonacci numbers

Kotlin
val fibonacci = sequence {
    var a = 0
    var b = 1
    yield(a)
    yield(b)
    while (true) {
        val next = a + b
        yield(next)
        a = b
        b = next
    }
}

fun main() {
    println(fibonacci.take(10).toList())  // Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}

4. Pattern Matching with Sealed Classes

Sealed classes allow you to represent restricted class hierarchies, providing more control over subclassing.

4.1. Creating Sealed Class Hierarchies

Define a sealed class and its subclasses:

Example: Defining a sealed class hierarchy for shapes

Kotlin
sealed class Shape
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()

fun main() {
    val circle = Circle(3.0)
    val rectangle = Rectangle(4.0, 5.0)
    println(circle)      // Output: Circle(radius=3.0)
    println(rectangle)   // Output: Rectangle(width=4.0, height=5.0)
}

4.2. Using when Expressions for Pattern Matching

Use when expressions to handle different cases of sealed class instances:

Example: Describing shapes using when expressions

Kotlin
sealed class Shape
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()

fun describeShape(shape: Shape): String = when (shape) {
    is Circle -> "Circle with radius ${shape.radius}"
    is Rectangle -> "Rectangle with width ${shape.width} and height ${shape.height}"
}

fun main() {
    val shape: Shape = Circle(2.5)
    println(describeShape(shape))  // Output: Circle with radius 2.5
}
This overview introduces key functional programming patterns in Kotlin, including mapping and transformation, reducing and folding, laziness and sequences, and pattern matching with sealed classes. These patterns enable writing clean, expressive, and efficient Kotlin code. A more detailed exploration of each section will provide deeper insights and examples for mastering functional programming in Kotlin.