Skip to Content
Course content

25: Lambda Expressions and Higher-Order Functions

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen a lot of developers hit a wall when they start building business logic that requires a bit of flexibility. Usually, it happens when you're writing something like a data filtering system. You start with one requirement, then another, and suddenly your class is bloated with a dozen nearly identical methods. Let's look at how that usually happens and how we can fix it using lambdas and higher-order functions.

The Method Bloat Trap

Imagine we're building a simple financial tracker. We have a Transaction data class, and we need to filter our list of transactions based on different criteria. The intuitive, "naive" approach is to write a specific function for every single filter the product owner asks for.

data class Transaction(val description: String, val amount: Double, val category: String)

class TransactionManager {
    fun filterByCategory(list: List, category: String): List {
        return list.filter { it.category == category }
    }

    fun filterByMinAmount(list: List, min: Double): List {
        return list.filter { it.amount >= min }
    }

    fun filterByDescription(list: List, query: String): List {
        return list.filter { it.description.contains(query, ignoreCase = true) }
    }
}

At first, this feels clean. But here is the problem: logic is leaking. Every time you need a new filter—say, transactions from a specific date range or those marked as "pending"—you have to modify the TransactionManager class, add a new method, and recompile. You're essentially hard-coding every possible business rule into the core of your manager. It's brittle, and frankly, it's boring to write.

Passing Logic as Data

The "pro" move here is to realize that the only thing changing between those three functions is the condition used to decide if a transaction stays or goes. In Kotlin, we can treat that condition as a first-class citizen. We can pass a function as an argument to another function. This is what we call a Higher-Order Function.

Instead of writing five different filter methods, we write one that accepts a lambda expression—a small, anonymous block of code that takes a Transaction and returns a Boolean.

class TransactionManager {
    // This is the Higher-Order Function. 
    // The 'predicate' parameter is actually a function: (Transaction) -> Boolean
    fun filterTransactions(list: List, predicate: (Transaction) -> Boolean): List {
        return list.filter(predicate)
    }
}

// Now, look how we use it:
val manager = TransactionManager()
val txs = listOf(Transaction("Coffee", 4.50, "Food"), Transaction("Rent", 1200.0, "Housing"))

// We define the logic on the fly using { }
val expensiveOnes = manager.filterTransactions(txs) { it.amount > 100.0 }
val foodOnly = manager.filterTransactions(txs) { it.category == "Food" }
val coffeeSearch = manager.filterTransactions(txs) { it.description.contains("Coffee") }

I love this approach because TransactionManager no longer cares how you filter; it only cares that you've provided a rule to do so. You've decoupled the mechanism of filtering from the criteria of the filter.

The Cost of Flexibility

Now, I wouldn't be doing my job if I didn't mention the trade-offs. Lambdas aren't a free lunch. In a very tight loop—like processing millions of transactions per second—creating these function objects can introduce a slight overhead. If you're seeing a performance hit, Kotlin provides the inline keyword. By marking your higher-order function as inline, the compiler effectively copies the lambda code directly into the call site, erasing the object overhead.

But for 95% of your app's code, don't over-optimize. The mental clarity of having one flexible function instead of twenty rigid ones is worth far more than a few nanoseconds of CPU time. You're trading a tiny bit of memory for a massive gain in maintainability.




📋 Practical Task

Build a Flexible Permission Guard

You are building a security module for an admin dashboard. Instead of writing separate checks for isAdmin(), isEditor(), and isOwner(), you need to implement a single PermissionGuard class that can handle any custom permission logic passed to it.

Your task:

  • Create a User data class with properties for username, role, and accessLevel (Int).
  • Implement a PermissionGuard class with a higher-order function called checkAccess. This function should take a User object and a predicate lambda (User) -> Boolean. It should return a String: "Access Granted" if the lambda returns true, and "Access Denied" otherwise.
  • In your main function, use this guard to test three different scenarios using lambdas:
    1. Grant access if the user is an "Admin".
    2. Grant access if the user's accessLevel is greater than 5.
    3. Grant access if the username is "SuperUser".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.