Blog Infos
Author
Published
Topics
Published
Topics
Deep Dive with Real World Examples for More Expressive and Efficient Code

Evolving with Kotlin — This image was created with the assistance of DALL·E 3

 

Introduction

The software development landscape is ever-changing, demanding developers not just to adapt, but to evolve. Kotlin, with its expressive syntax and robust features, has quickly become a trusted ally for many in this journey. While its initial appeal may come from its concise syntax and interoperability with Java, Kotlin’s real strength lies in its deeper, functional programming capabilities. These techniques, once mastered, have the potential to transform the way we approach problems, design solutions, and even perceive code.

This article dives deep into advanced functional programming in Kotlin, providing insights and real-world examples that aim to elevate your coding prowess. Whether you’re refining your skills or taking your first steps into this domain, here’s a guide tailored to resonate with the challenges and aspirations of modern developers.

Kotlin’s Functional Foundation

The core of functional programming in Kotlin hinges on the concepts of immutability and treating functions as first-class citizens.

1. Immutable Data Structures

Basic Syntax —

In Kotlin, the val keyword denotes a read-only (immutable) variable. While the variable itself is immutable, the data it points to may not be. This is why Kotlin also offers immutable collections.

val readOnlyList = listOf("a", "b", "c")

Real World Example —

Consider a typical e-commerce application. When a user views their profile, they see a list of their past orders. To prevent accidental modifications when displaying these orders, it’s safer to ensure the order list remains immutable.

data class Order(val orderId: Int, val product: String, val price: Double)

// Suppose we fetch this from a database or an API
val userOrders: List<Order> = fetchOrdersFromDatabase()

// If we later want to give a discount, instead of modifying the original list, 
// we create a new list with updated prices.
val discountedOrders = userOrders.map { order ->
    if (order.price > 100.0) {
        order.copy(price = order.price * 0.9)  // 10% discount
    } else {
        order
    }
}
2. First Class Functions

Basic Syntax —

Kotlin’s support for first-class functions means they can be assigned to variables, passed around as arguments, or returned from other functions.

fun greet(name: String) = "Hello, $name!"
val greetingFunction: (String) -> String = ::greet
println(greetingFunction("Bob"))  // Outputs: Hello, Bob!

Real World Example —

In a graphics rendering software, various effects (like blur, sharpen, or color inversion) can be applied to an image. By treating functions as first-class citizens, these effects can be represented as functions and combined in various ways.

fun blur(image: Image): Image = ...
fun sharpen(image: Image): Image = ...
fun invertColors(image: Image): Image = ...

val effects = listOf(::blur, ::sharpen, ::invertColors)

// Apply all effects on an image sequentially
val processedImage = effects.fold(originalImage) { img, effect -> effect(img) }

 

OUR VIDEO RECOMMENDATION

Jobs

No results found.

Advanced Collection Functions

Kotlin offers a rich set of functions to operate on collections. Beyond the basics, understanding the intricacies of these functions can dramatically improve code clarity and efficiency.

Photo by Karoline Stk on Unsplash

1. Transformations with map and flatMap

Basic Syntax —

The map function transforms each element in a collection using a provided transformation function. flatMap, on the other hand, can transform and flatten collections.

val numbers = listOf(1, 2, 3)
val squared = numbers.map { it * it }  // [1, 4, 9]

Real World Example —

Suppose you have a list of strings representing potential URLs, and you want to extract the domain names. Not every string is a valid URL, so this is where flatMap comes into play.

val potentialUrls = listOf("https://example.com/page", "invalid-url", "https://another-example.com/resource")

val domains = potentialUrls.flatMap { url ->
    runCatching { URL(url).host }.getOrNull()?.let { listOf(it) } ?: emptyList()
}
// Result: ["example.com", "another-example.com"]
2. Filtering with filter and filterNot

Basic Syntax —

filter returns a list of elements that satisfy the given predicate. filterNot does the opposite.

val numbers = listOf(1, 2, 3, 4, 5)
val evens = numbers.filterNot { it % 2 == 0 }  // [1, 3, 5]

Real World Example —

Imagine filtering products not just based on a single condition, but multiple dynamic conditions (like price range, rating, and availability).

data class Product(val id: Int, val price: Double, val rating: Int, val isAvailable: Boolean)

val products = fetchProducts()  // Assume this fetches a list of products

val filteredProducts = products.filter { product ->
    product.price in 10.0..50.0 && product.rating >= 4 && product.isAvailable
}
3. Accumulation using fold and reduce

Both fold and reduce are used for accumulative operations, but they serve slightly different purposes and are used in distinct scenarios.

We will start with fold

  • Purpose — Performs an operation on elements of the collection, taking an initial accumulator value and a combining operation. It can work on collections of any type, not just numeric types.
  • Basic Syntax —
val numbers = listOf(1, 2, 3, 4)
val sumStartingFrom10 = numbers.fold(10) { acc, number -> acc + number }  // Result: 20
  • Example — For instance, if you want to concatenate strings with an initial value
val words = listOf("apple", "banana", "cherry")
val concatenated = words.fold("Fruits:") { acc, word -> "$acc $word" }
// Result: "Fruits: apple banana cherry"

Let’s look at reduce

  • Purpose — Similar to fold, but it doesn’t take an initial accumulator value. It uses the first element of the collection as the initial accumulator.
  • Basic Syntax —
val numbers = listOf(1, 2, 3, 4)
val product = numbers.reduce { acc, number -> acc * number }  // Result: 24
  • Example— Combining custom data structures. Consider a scenario where you want to combine ranges:
val ranges = listOf(1..5, 3..8, 6..10)
val combinedRange = ranges.reduce { acc, range -> acc.union(range) }
// Result: 1..10

Key Differences —

  1. Initial Value —
  • fold takes an explicit initial accumulator value.
  • reduce uses the first element of the collection as its initial value.

2. Applicability —

  • fold can work with collections of any size, including empty collections (because of the initial accumulator value).
  • reduce throws an exception on empty collections since there’s no initial value to begin the operation with.

3. Flexibility —

  • fold is more flexible as it allows defining an initial value that can be of a different type than the collection’s elements.
  • reduce has the type constraint that the accumulator and the collection’s elements must be of the same type.
4. Partitioning with groupBy and associateBy

Basic Syntax —

groupBy returns a map grouping elements by the results of a key selector function. associateBy returns a map where each element is a key according to the provided key selector.

val words = listOf("apple", "banana", "cherry")
val byLength = words.groupBy { it.length }  // {5=[apple], 6=[banana, cherry]}

Real World Example —

data class Student(val id: String, val name: String, val course: String)

val students = fetchStudents()

// Assume students contains:
// Student("101", "Alice", "Math"), Student("101", "Eve", "History"), Student("102", "Bob", "Science")

val studentsById = students.associateBy { it.id }
// The resulting map would be:
// {"101"=Student("101", "Eve", "History"), "102"=Student("102", "Bob", "Science")}

In the above example, Eve overwrote Alice because they both have the ID “101”. The resulting map only retains the details of Eve, the last entry with that ID in the list.

Key Difference —

  • groupBy creates a Map where each key points to a List of items from the original collection.
  • associateBy creates a Map where each key points to a single item from the original collection. If there are duplicates, the last one will overwrite the others.

When deciding between the two, consider whether you need to preserve all elements with the same key (groupBy) or just the last one (associateBy).

Function Composition in Kotlin

Photo by Mohit Suthar on Unsplash

Imagine you have a toy factory assembly line, and at each station on this line, the toy undergoes a specific change. The toy moves from one station to the next, being modified at each step.

In programming, especially in Kotlin, this is when you take two functions and chain them together, so the result of the first function becomes the input of the next.

Imagine our toy factory has three stations —

  1. Station A — Paints a toy.
  2. Station B — Attaches wheels to the painted toy.
  3. Station C — Places a sticker on the toy with wheels.

These stations are like functions, each performing its task in sequence.

In Kotlin, let’s represent these stations as functions —

fun paint(toy: Toy): Toy { /* paints the toy and returns it */ }
fun attachWheels(toy: Toy): Toy { /* attaches wheels and returns the toy */ }
fun placeSticker(toy: Toy): Toy { /* places a sticker and returns the toy */ }

Instead of manually moving the toy from one station to the next, we want an automated process where the toy flows smoothly from the start to the end. This is where function composition comes into play.

To make this work in Kotlin, we’ll define a compose function —

infix fun <A, B, C> ((B) -> C).compose(g: (A) -> B): (A) -> C {
    return { x -> this(g(x)) }
}

This compose function is our tool to link two stations (functions) together. It ensures that the output of one station becomes the input of the next.

Now, using the compose function, we can define our automated toy assembly line —

val completeToyProcess = ::placeSticker compose ::attachWheels compose ::paint

When you place a raw toy into this completeToyProcess, it will automatically get painted, have wheels attached, and then receive a sticker.

Example in Action —
val rawToy = Toy()
val finishedToy = completeToyProcess(rawToy)

In this example, rawToy goes through the entire process and comes out as finishedToy — painted, with wheels, and a sticker, all in one smooth operation.

Why is this Useful?
  1. Clarity — Just like in our toy factory analogy, you can see the entire assembly line process in one go. You can quickly understand the sequence of changes the toy undergoes.
  2. Flexibility — You can easily change the order or add/remove a station (or function) if you need a different result.
  3. Efficiency — You don’t have to store the toy after each modification; it just keeps moving through the assembly line.
Anything to Watch Out For?

Yes, a couple of things —

  1. Order Matters — Just like you can’t place a sticker on the toy before it’s even painted, the order in which you chain the functions is crucial.
  2. Keep it Simple — If your assembly line (or chain of functions) gets too long, it can become hard to understand or manage. It’s like having too many stations in our toy factory. So, balance is key!
Currying — The Power of Incremental Decisions

Photo by Nathan Dumlao on Unsplash

Imagine you’re at a versatile coffee shop. Instead of choosing a ready-made beverage, they offer you a series of choices. First, you pick the type of coffee bean, then decide on the milk (or milk alternative), and finally, select any additional flavors or toppings.

Now, suppose you’re a regular who always starts with an Arabica bean but varies other choices based on mood. Instead of making you start from scratch each time, the coffee shop remembers your bean preference. This approach saves time, reduces decision fatigue, and lets you focus on what’s important at the moment.

This is akin to what currying achieves in programming.

Breaking It Down
  1. Simplifying Complex Decisions: Just as picking a coffee involves several steps, some functions have many parameters. Currying breaks down these multi-parameter functions into a chain of simpler functions. Each function in this chain takes a single argument and returns the next function to be called with another argument.
  2. Remembering Preferences: By currying a function, you can ‘remember’ certain decisions (or function arguments). In our coffee example, your preference for Arabica beans is remembered, letting you play with other choices.
  3. Focus on What’s Important: Sometimes, you don’t have all the information up front. Currying allows you to make decisions as information becomes available. It’s like deciding on your coffee’s milk and flavors later when you’re at the counter, even though you chose the bean type days ago.
In Code

Suppose there’s a function to order a coffee.

fun orderCoffee(bean: String, milk: String, flavor: String): Coffee { ... }

Using currying, it becomes —

fun orderCoffee(bean: String): (String) -> (String) -> Coffee { ... }

With this curried function, if you know you want Arabica beans, you can make a decision for just that:

val arabicaOrder = orderCoffee("Arabica")

Later, when you decide on the milk and flavor, you continue —

val myCoffee = arabicaOrder("Almond Milk")("Vanilla")
Why Currying Shines
  1. Modularity — Currying promotes modular design, letting you focus on one piece of logic at a time.
  2. Reusability — Like the Arabica bean preference, currying can remember certain decisions, enabling code reuse.
  3. Dynamic Function Creation — You can create specialized functions on-the-fly based on the context or user preferences.

Through currying, developers can build more adaptable, modular, and user-centric systems. Just as our coffee shop tailors its process to fit the customer’s journey, currying customizes the function-calling process to the developer’s needs.

Monads — The Safety Nets of Programming

Photo by Inside Weather on Unsplash

 

Imagine assembling a DIY furniture kit. Each step in the instruction manual is dependent on the prior one. However, not all steps are as straightforward, and sometimes, you might find a piece missing or realize you made an error in a previous step.

Wouldn’t it be wonderful if the manual came with built-in safety nets? For instance, if you’re about to fix a screw in the wrong place, the manual instantly alerts you. Or, if a piece is missing, it offers a workaround or tells you how to proceed without it.

This concept of “safety nets” in the world of DIY is what monads bring to programming.

Understanding Monads
  1. Dependent Steps — Just as furniture assembly involves a sequence of dependent steps, operations in programming are often a chain where each link relies on the success of the one before.
  2. Safety Mechanism — Monads act as a safety mechanism, ensuring that if one step fails or doesn’t produce a valid value, the subsequent steps are made aware and can react accordingly.
  3. Encapsulating Challenges — Monads bundle together values with the context of how those values were produced, be it successfully, with errors, or through some side-effects.
A Practical Dive

Kotlin’s Optional is a form of monad. Imagine querying a database for a user’s profile —

fun findUserProfile(id: Int): Optional<UserProfile> {
    // Some logic to fetch profile
}

Let’s say we want to retrieve the user’s email —

val emailOpt = findUserProfile(123).flatMap { profile -> profile.email }

If findUserProfile doesn’t find the profile, it might return an empty Optional. The flatMap operation won’t crash or throw an error; it’ll simply produce another empty Optional.

This is akin to our DIY manual’s safety net. If a step can’t be completed, it doesn’t halt the entire process but gives you a way to proceed safely.

Monads in the Limelight
  1. Graceful Failure: Monads allow for functions to fail gracefully. Instead of abrupt crashes or halts, they ensure the process moves forward, even if it’s to convey an error.
  2. Intuitive Flow: With monads, the code flow becomes more intuitive and reflective of real-life decision-making processes.
  3. Enhanced Composability: Thanks to their chainable nature, monads lead to more modular and adaptable code.
Lazy Evaluation and Sequences — Powering Efficient Operations

Photo by Shreyak Singh on Unsplash

 

Ever been to a buffet and decided to only take the dishes you’re sure to eat, rather than filling your plate all at once and potentially wasting food? This strategy allows you to consume what you need, when you need it, ensuring maximum enjoyment with minimal waste.

Lazy evaluation in programming adopts a similar strategy. Instead of computing everything upfront, you calculate only what’s necessary, when it’s necessary. In Kotlin, sequences are the primary way to achieve this. Let’s dive in!

Understanding Lazy Evaluation

Lazy evaluation is a computation strategy where expressions are evaluated only when their result is actually needed. This can lead to more efficient memory use and faster execution, especially when working with large collections.

Kotlin Sequences

In Kotlin, sequences (Sequence<T>) represent a lazily-evaluated collection. Unlike lists, sequences don’t hold data; instead, they describe computations to produce data elements when requested.

In Practice — Sequences vs. Lists

Consider a list of numbers, and we want to find the first number that’s divisible by 5 after being squared.

Using a list —

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val result = numbers.map { it * it } // squares all numbers
                .filter { it % 5 == 0 } // filters all squared numbers divisible by 5
                .first() // fetches the first item
println(result)  // 25

Now, in this approach, we square all numbers and filter them, only to use one value. That’s inefficient!

Using a sequence —

val numbersSeq = numbers.asSequence()

val resultSeq = numbersSeq.map { it * it }
                          .filter { it % 5 == 0 }
                          .first()

println(resultSeq)  // 25

With sequences, each number is squared, checked if it’s divisible by 5, and then the process halts when the first such number is found. So, in this case, the sequence only squares and filters until it finds the number 5. That’s efficient!

Benefits of Lazy Evaluation with Sequences
  1. Efficiency — Only compute what’s necessary.
  2. Flexibility — Can represent infinite data structures.
  3. Memory Conservation — Especially important when dealing with large datasets.

Embracing sequences and lazy evaluation in Kotlin can be likened to adopting a “consume as you go” approach. It empowers developers to write efficient and scalable code, especially in scenarios with extensive data operations.

Tail Recursion — Harnessing Kotlin for Efficient Recursion

Photo by Ruffa Jane Reyes on Unsplash

 

Picture this — You’re standing on the ground floor of a tall building, gazing upwards at its never-ending staircase. If you were to climb each step, one by one, you might tire out quickly or find yourself overwhelmed. But what if you could jump multiple floors in a single bound, while using the same energy as climbing a single step? That’s the magic of tail recursion in Kotlin!

Breaking Down Recursion

Recursion is a programming technique where a function calls itself to break down complex problems into simpler ones. However, standard recursion can quickly consume a lot of memory, especially for large inputs. Each function call gets added to the call stack, and with deep recursion, this could lead to a stack overflow error.

Introducing Tail Recursion

Tail recursion is a specialized form of recursion where the recursive call is the last thing executed in the function. Kotlin’s compiler optimizes tail-recursive functions to use constant stack space, preventing stack overflow errors.

Simple Example — Factorial

Without tail recursion —

fun factorial(n: Int): Int {
    if (n == 1) return 1
    return n * factorial(n - 1)
}

With tail recursion —

fun factorial(n: Int, accumulator: Int = 1): Int {
    if (n == 1) return accumulator
    return factorial(n - 1, n * accumulator)
}

In the tail-recursive version, the result of the recursive call (combined with the current operation) is passed as an accumulator. It ensures that no extra operations are pending after the recursive call, making it a valid tail call.

Why Use Tail Recursion?
  1. Efficiency — It uses constant stack space, preventing stack overflows.
  2. Clarity — Recursive solutions can be more intuitive for certain problems.
  3. Kotlin’s Support — By just adding the tailrec modifier, Kotlin takes care of the optimizations!
Important Note

It’s essential to ensure that recursion is truly in tail position. If any operations are pending post the recursive call, the function won’t be tail-recursive, and Kotlin’s compiler won’t optimize it.

What’s Happening Behind the Scenes with Tail Recursion?

In traditional recursion, each function call would be stacked, waiting for the next one to complete before finalizing its own computations. This would pile up memory usage as the function calls stack up, especially with large input numbers.

In our tail-recursive version, here’s what happens —

  1. Each recursive call is optimized to reuse the current function’s stack frame because no computation (like multiplication in the case of factorial) is left after the recursive call.
  2. The accumulator acts as a running total, holding the intermediate result. This means that, by the time we reach the base case (n == 1), we already have our answer in the accumulator, and there’s no need to “work our way back up.”
  3. The Kotlin compiler sees the tailrec modifier and recognizes that the function is tail-recursive. It then optimizes the bytecode behind the scenes to ensure that the function uses a constant amount of stack memory regardless of input size.

In essence, our factorial function, when called with factorial(5), essentially transforms the computation from:

5 * 4 * 3 * 2 * 1

to:

(((5 * 1) * 4) * 3) * 2

This transformation ensures the result is ready as soon as the base case is reached, all while using a constant stack space.

Another Note

While tail recursion optimization is a powerful feature in Kotlin, it’s worth noting that this concept is not exclusive to the language. Many other programming languages, ranging from functional ones like Haskell to more general-purpose ones like Scala, offer support for tail recursion. However, the way they implement and optimize it can differ. Always consider this when transitioning between languages or discussing the topic with developers from varied backgrounds.

Conclusion

Timeless — Photo by Gaurav D Lathiya on Unsplash

 

Throughout our exploration of advanced functional programming in Kotlin, we’ve seen the depth and versatility Kotlin offers. From the intricacies of collection functions, the elegance of function composition, to the efficiency of tail recursion, Kotlin equips developers with powerful tools. These concepts, while highlighted in Kotlin, are pillars in the broader functional programming world. By mastering them, you’re not only optimizing your Kotlin skills but also tapping into timeless programming principles. As you venture forward, let these tools and techniques guide your Kotlin journey to produce more efficient, clean, and maintainable code.

Closing Remarks

If you liked what you read, please feel free to leave your valuable feedback or appreciation. I am always looking to learn, collaborate and grow with fellow developers.

If you have any questions feel free to message me!

Follow me on Medium for more articles — Medium Profile

Connect with me on LinkedInand Twitter for collaboration.

Happy Coding!

 

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu