-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
149: Function Types and Higher-Order Type Signatures
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
NotificationFormatterinterface and the two implementation classes. - Modify the
sendNotificationfunction to accept a function type as theformatterparameter instead of an interface. - Call
sendNotificationtwice in yourmainfunction: once using a lambda for SMS formatting and once using a lambda for Email formatting.
There are no comments for now.