1. Why fun interface is needed
Once you have gotten used to lambdas (like { x -> x > 0 } ) and function types ( (Int) -> Boolean ), it may seem that a separate construct like fun interface is “abstraction for abstraction’s sake.” But in practice it solves a very down-to-earth problem: how to name behavior and how to agree on it in an API, so that code reads like normal text rather than a pile of parentheses.
Imagine you are writing an application where you have a “command handler,” a “validation rule,” a “formatting strategy,” and so on. Formally, these are all functions. But if you keep dragging around types like (AppState, List<String>) -> String, your brain starts to overheat. fun interface lets you say: “this type is a command handler,” and things become clearer right away.
Formally, in Kotlin a functional (SAM) interface is an interface with one abstract method, and Kotlin can “substitute” a lambda as its implementation (SAM conversion).
SAM in plain English: “one action” as a contract
Let’s translate SAM from spec-speak into real life. A regular interface often describes a “creature with many responsibilities”: for example, “can draw,” “can be saved,” “can report its name,” “can be closed.” That is normal.
A SAM interface is an interface about one action, like a button with a single function. For example: “check a number,” “handle an event,” “format a string,” “execute a command.” You need it when you want a named contract (so function signatures read well), but you want to provide the implementation quickly—with a lambda, without the ritual object : ....
In Kotlin, a functional/SAM interface can have any number of non-abstract functions, but the abstract method must be exactly one.
Why not just a function type
A natural question here is: “Why introduce fun interface CommandHandler if you can write typealias CommandHandler = (AppState, List<String>) -> String?”
The point is that a functional interface is not the same as a typealias for a function type. An interface defines a named contract and can have additional non-abstract methods, while a typealias is just an alias for an existing type.
From a practical perspective, the difference often looks like this:
| What we choose | When it is convenient | What we get |
|---|---|---|
| Function type (A) -> B | Internal logic, “passed quickly and forgot” | Minimum entities, maximum brevity |
| fun interface | Public API, “this is an important architectural role” | A meaningful type name + the ability to extend the interface with default methods |
So fun interface is a way to make behavior named and domain-specific. When you read CommandHandler, you understand the role. When you read (AppState, List<String>) -> String, you only understand the shape (and not always quickly).
2. How to declare a fun interface and how SAM conversion works
The key detail is literally the keyword fun before interface. This is not “functional interface” in the sense of “somehow related to FP,” but a technical marker: “compiler, please allow SAM conversion and enforce the single-abstract-method rule.”
Minimal example:
fun interface IntPredicate {
fun accept(x: Int): Boolean
}
Here accept is the only abstract method. Therefore Kotlin can do this:
fun interface IntPredicate {
fun accept(x: Int): Boolean
}
fun main() {
val isEven = IntPredicate { x -> x % 2 == 0 }
println(isEven.accept(7)) // false
println(isEven.accept(10)) // true
}
The important idea is: we are creating an interface object; Kotlin just lets you describe it with a single lambda.
What exactly is “converted” in SAM conversion
When people say “a lambda turns into an interface,” it is easy to imagine something mystical—like the lambda suddenly becomes a class. In reality, it is a very understandable trick: the compiler generates an object that implements the interface and plugs your lambda in as the body of the single method.
You can visualize it like this:
flowchart LR
A["Lambda: { x -> x % 2 == 0 }"] --> B["SAM conversion by the compiler"]
B --> C["Object implementing IntPredicate"]
C --> D["Method accept(x) calls the lambda"]
So SAM conversion is a way to tell the compiler: “generate an implementation of the interface for me, and here is the logic.” This is especially handy when you want to store or pass around not “just a function,” but an object with a specific contract.
object : ... vs a SAM lambda: same meaning, different syntax
Typically, the value of SAM conversion becomes obvious when you see two code snippets side by side: one “like Java in the anonymous-class era,” the other “like Kotlin.”
The same meaning (evenness check) without SAM conversion:
fun interface IntPredicate {
fun accept(x: Int): Boolean
}
fun main() {
val isEven = object : IntPredicate {
override fun accept(x: Int): Boolean = x % 2 == 0
}
println(isEven.accept(7)) // false
}
And with SAM conversion:
fun interface IntPredicate {
fun accept(x: Int): Boolean
}
fun main() {
val isEven = IntPredicate { it % 2 == 0 }
println(isEven.accept(7)) // false
}
Both variants create an object that can accept. But in the second variant you do not write extra code.
3. Example: CLI command handlers with a fun interface
Now let’s tie the topic to something real. Suppose we have a console app for tracking expenses (let’s call it BudgetBuddy). Before, we would most likely make a big when (command) and stuff the logic inside. That works, but it grows and starts to look like a monolith.
Let’s take a step forward: move command handlers into a data structure (for example, a Map), but do it in such a way that the handler is a named contract, not a bare function. A fun interface is a perfect fit for this.
Minimal models and the handler contract
First, some simple models:
data class Expense(
val id: Int,
val title: String,
val amount: Double
)
class AppState(
val expenses: MutableList<Expense>
)
Now let’s declare the command handler contract:
fun interface CommandHandler {
fun handle(state: AppState, args: List<String>): String
}
Why do we return a String? Because for now we do not want to complicate the result model. Let the handler simply return text that main will print. This is convenient for learning and does not require new entities.
Let’s make a rendering utility:
fun renderExpenses(expenses: List<Expense>): String {
if (expenses.isEmpty()) return "No expenses yet."
return expenses.joinToString(separator = "\n") { e ->
"#${e.id}: ${e.title} — ${e.amount}"
}
}
Now create a “command registry” via SAM conversion:
fun buildHandlers(): Map<String, CommandHandler> {
return mapOf(
"help" to CommandHandler { _, _ ->
"Commands: help, add, list, exit"
},
"list" to CommandHandler { state, _ ->
renderExpenses(state.expenses)
}
)
}
Notice an important point: CommandHandler { state, args -> ... } is creating an object handler via a lambda.
Invoking handlers in main
Now we move on to the command read loop. We will not build a big parser; we will make a simple version: the command is the first word, the arguments are the rest.
fun main() {
val state = AppState(mutableListOf())
val handlers = buildHandlers()
while (true) {
print("> ")
val parts = readln().trim().split(" ")
val cmd = parts.firstOrNull()?.lowercase() ?: continue
val args = parts.drop(1)
if (cmd == "exit") break
val handler = handlers[cmd]
val result = handler?.handle(state, args) ?: "Unknown command: $cmd"
println(result)
}
}
The conceptual win here is that main now deals with dispatching, not command logic. It reads like “read → find handler → call → print,” while the details live in the handlers.
When “why won’t it compile”: the signature must match
When SAM conversion “doesn’t kick in,” the reason is almost always simple: the lambda does not match the parameters or return type of the abstract method.
For example:
fun interface IntTransformer {
fun apply(x: Int): Int
}
The correct way:
val plusOne = IntTransformer { it + 1 }
println(plusOne.apply(10)) // 11
But this will not work (we return Boolean, but we need Int):
val wrong = IntTransformer { it > 0 } // won't compile
For a beginner this may look like “the compiler is being picky.” In reality it is saving you from a weird reality where a handler “must return Int,” but you returned “true/false.”
fun interface and closures: convenient, but you can hide state
The trickiest part of lambdas is not syntax, but the fact that a lambda can “capture” external variables. This is called a closure (closures).
Example:
fun interface CounterStep {
fun next(): Int
}
fun main() {
var x = 0
val step = CounterStep {
x += 1
x
}
println(step.next()) // 1
println(step.next()) // 2
}
Technically, this works. But architecturally you have now created a handler that depends on an external var x. If such a handler moves into another file or module, it becomes harder to understand and test: the state is hidden not inside the object but “outside, nearby.”
Sometimes this is fine (for example, for a small counter), but in application code it helps to keep a rule in mind: if behavior becomes important and long-lived, it is better for state to be explicit (for example, inside a class / the application state structure), rather than accidentally captured.
4. Common mistakes when working with fun interface and SAM conversion
Mistake #1: making a fun interface with two abstract methods and being surprised that Kotlin complains.
SAM conversion is possible only if there is exactly one abstract method. As soon as you add a second abstract method, the interface stops being functional, and the lambda can no longer unambiguously “become the implementation.” This is not Kotlin being mean; it is protection against ambiguity.
Mistake #2: confusing a “functional interface” with a “function type,” and then losing the meaning of the API.
When inside a function you quickly filter a list, (Expense) -> Boolean is great. But if you design a layer with roles like “command handler” or “validator,” a function type turns into eye-cryptography. fun interface is needed precisely so that behavior has a name and reads as part of the domain.
Mistake #3: the lambda does not match the signature of the abstract method, and it seems like SAM doesn’t work.
SAM conversion works when the types match. If the method expects a String and you write a lambda that returns an Int, the compiler honestly says: “I can’t substitute this for that.” It often helps to explicitly type lambda parameters and see what is expected: { x: Int -> ... } instead of { it -> ... }.
Mistake #4: capturing an external var in a lambda and ending up with hidden mutability.
A closure is a normal mechanism by itself. But if you start keeping important application state in closures, the code becomes hard to debug: it is unclear who changes the variable and when. It is better to keep the “official” state in AppState/classes, and use lambdas as pure (or almost pure) actions.
Mistake #5: turning a fun interface into a “method dump,” losing the “one action” idea.
Yes, a functional interface can have additional methods, but they must be non-abstract. In practice, this is a signal: if you started adding lots of logic and different responsibilities, maybe it is no longer a SAM contract but a regular interface. SAM is good precisely because it is “single-operation”: one method—one responsibility.