Skip to Content
Course content

149: Function Types and Higher-Order Type Signatures

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

I've seen a lot of developers move from Java to Kotlin and bring a certain "interface habit" with them. Whenever they need to pass logic around—say, a strategy for filtering a list or a rule for validating a user—they immediately reach for an interface. Now, don't get me wrong; interfaces are powerful, but when you're just trying to pass a single piece of logic, they often feel like wearing a tuxedo to a backyard BBQ. It's overkill.

The Interface Explosion

Imagine we're building a payment processing system. We need to apply different validation rules depending on the payment method. The "classic" way to handle this is to define a validator interface and then implement it multiple times.

interface PaymentValidator {
    fun isValid(payment: Payment): Boolean
}

class CreditCardValidator : PaymentValidator {
    override fun isValid(payment: Payment) = payment.cardNumber.length == 16
}

class PayPalValidator : PaymentValidator {
    override fun isValid(payment: Payment) = payment.email.contains("@")
}

fun processPayment(payment: Payment, validator: PaymentValidator) {
    if (validator.isValid(payment)) {
        println("Payment processed!")
    }
}

This works, but look at the friction. If I want to add a new validation rule—like checking if the payment amount is positive—I have to create a whole new class. Even if it's only two lines of code, I'm adding a new file or a new class definition to my project. It creates this "interface explosion" where your project structure becomes cluttered with tiny classes that only ever do one thing.

Trading Boilerplate for Function Signatures

Kotlin gives us a way to treat functions as first-class citizens. Instead of defining an interface with one method, we can define a function type. A function type is essentially a signature that says, "I don't care who you are, as long as you take these inputs and return this output."

In the payment example, our PaymentValidator is really just a function that takes a Payment and returns a Boolean. In Kotlin, that type is written as (Payment) -> Boolean. Let's see how that changes the architecture:

fun processPayment(payment: Payment, validator: (Payment) -> Boolean) {
    if (validator(payment)) {
        println("Payment processed!")
    }
}

// Now I can just pass a lambda directly
processPayment(myPayment) { it.cardNumber.length == 16 }
processPayment(myPayment) { it.email.contains("@") }
processPayment(myPayment) { it.amount > 0 }

The difference here is massive. I've eliminated three classes and an interface. I've replaced them with a type signature. This is what we call a Higher-Order Function—a function that takes another function as a parameter. It shifts the focus from "What object is this?" to "What behavior does this perform?"

Where the Trade-off Hits

You might be wondering why we'd ever go back to interfaces. I'll be honest: function types aren't a silver bullet. If your "strategy" needs three different methods (e.g., validate(), cleanup(), and logError()), a single function type won't cut it. You can't have a function type that represents a multi-method contract. In those cases, the interface is the correct tool.

Also, keep an eye on readability. If your function signature starts looking like (Int, String, (Double) -> Boolean) -> List<Int>, you've gone too far. At that point, the "naive" approach of using a named interface or a data class to wrap those parameters becomes the "better" way because it provides semantic meaning to the arguments. I usually suggest a rule of thumb: if the function type is a simple mapping or a predicate, use the signature. If it's a complex contract, use an interface.




📋 Practical Task

Refactoring the Notification Dispatcher

You have been handed a legacy piece of code for a notification system. Currently, it uses a clumsy interface-based approach to handle different notification formats (SMS, Email, Push). Your task is to refactor this to use Function Types and a Higher-Order Function to eliminate the boilerplate.

Current Code:

interface NotificationFormatter {
    fun format(message: String, user: User): String
}

class SmsFormatter : NotificationFormatter {
    override fun format(message: String, user: User) = "SMS to ${user.phone}: $message"
}

class EmailFormatter : NotificationFormatter {
    override fun format(message: String, user: User) = "Email to ${user.email}: $message"
}

fun sendNotification(message: String, user: User, formatter: NotificationFormatter) {
    val finalMessage = formatter.format(message, user)
    println("Sending: $finalMessage")
}

Requirements:

  • Remove the NotificationFormatter interface and the two implementation classes.
  • Modify the sendNotification function to accept a function type as the formatter parameter instead of an interface.
  • Call sendNotification twice in your main function: once using a lambda for SMS formatting and once using a lambda for Email formatting.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.