Baeldung Pro – Kotlin – NPI EA (cat = Baeldung on Kotlin)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Introduction

In Kotlin, functions are first-class citizens, meaning we can store them in variables, pass them as parameters, or even return them. This flexibility enhances code reuse and modularity. However, dealing with overloaded functions—functions sharing the same name but differing in parameters—can lead to ambiguity when referencing them.

When referencing an overloaded function, the Kotlin compiler may not be able to infer which specific function we intend to use, resulting in a compilation error. This ambiguity can be frustrating, but Kotlin provides several techniques to resolve it.

In this tutorial, we’ll explore techniques for explicitly specifying function types, using lambdas, leveraging extension functions, and employing higher-order functions.

2. Overloaded Functions Example

To illustrate the problem, let’s consider a Calculator class with overloaded compute() methods:

class Calculator(val base: Int) {
    fun compute(value: Int): Int = base + value  
    fun compute(value: String): String = "$base$value"  
}

The overloaded compute() methods either sum or concatenate our base value with another Int or String.

If we try to reference the compute() method directly without the input type, the compiler can’t determine which version to use:

fun main() {
    val calculator = Calculator(10)
    val compute = calculator::compute
}

This results in a compilation error because the compiler can’t infer which compute() method to reference. In the following sections, we’ll explore techniques to resolve this ambiguity.

3. Explicitly Specifying the Function Type

One way to resolve the ambiguity is by explicitly defining the function type when referencing the overloaded function. This makes it clear to the compiler which version of the function we intend to use:

private val calculator = Calculator(10)

@Test
fun `Should compute with int function`() {
    val computeInt: (Int) -> Int = calculator::compute
    assertEquals(15, computeInt(5))
}

@Test
fun `Should compute with int function`() {
    val computeString: (String) -> String = calculator::compute
    assertEquals("105", computeString("5"))
}

By explicitly specifying the function types (Int) -> Int and (String) -> String, we disambiguate the overloaded functions, allowing the compiler to identify the intended version. This approach is straightforward and leverages Kotlin’s type inference system effectively.

3.1. Using Type Aliases for Clarity

Type aliases can also resolve ambiguity by providing descriptive names for function types:

typealias IntComputer = (Int) -> Int
typealias StringComputer = (String) -> String

private val calculator = Calculator(10)

@Test
fun `should convert int with type alias`() {
    val computeInt: IntComputer = calculator::compute
    assertEquals(15, computeInt(5))
}

@Test
fun `should convert string with type alias`() {
    val computeString: StringComputer = calculator::compute
    assertEquals("105", computeString("5"))
}

By defining type aliases IntComputer and StringComputer, we provide descriptive names for the method types. This not only resolves function ambiguity but also improves code readability by making the intent clear.

The tests ensure the type aliases correctly reference the intended compute() method. In general, the type alias is the same concept as assigning a type to the ambiguous function reference but simplifies referencing a complex function type.

4. Using Lambdas

Instead of directly referencing the function, we can define typed lambdas to call the desired function explicitly. This provides clear context for the compiler:

private val calculator = Calculator(10)

@Test
fun `should compute int with lambda`() {
    val computeInt = { value: Int -> calculator.compute(value) }
    assertEquals(15, computeInt(5))
}

@Test
fun `should compute string with lambda`() {
    val computeString = { value: String -> calculator.compute(value) }
    assertEquals("105", computeString("5"))
}

These lambdas work by assigning types to their input type and providing the compiler with context to bypass the ambiguity. The tests ensure that the lambdas correctly invoke the intended compute method.

5. Leveraging Extension Functions

Another effective way to resolve ambiguity is using non-ambiguous extension functions to provide clear context for overloaded behavior. Instead of creating a new class, we can define extension functions on the existing Calculator class. These extension functions have unique names to eliminate ambiguity when referencing them:

fun Calculator.computeInt(value: Int): Int = compute(value)
fun Calculator.computeString(value: String): String = compute(value)

private val calculator= Calculator(10)

@Test
fun `should compute int with extension function`() {
    val computeInt = calculator::computeInt
    assertEquals(15, computeInt(5))
}

@Test
fun `should compute string with extension function`() {
    val computeString = calculator::computeString
    assertEquals("105", computeString("5"))
}

We define two non-ambiguous extension functions on the Calculator class: computeInt() and computeString(). These functions internally call the appropriate overloaded compute() method but provide unique names, eliminating ambiguity when referencing them. Using unique names ensures the compiler can always determine which function to use.

6. Conclusion

In this article, we’ve explored several techniques to resolve ambiguity in overloaded functions.

By explicitly specifying function types, using lambdas, leveraging extension functions, and employing type aliases, we can provide the compiler with the necessary context to determine the correct function. These techniques help avoid compiler errors and also enhance code clarity and maintainability.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.