Previously, we explored generic classes and their use of type parameters to write more reusable code. Now, let's turn our attention to generic functions. These functions allow us to operate on various types while preserving type safety. Like their class counterparts, they rely on type parameters to provide this flexibility.
Let's take a closer look!
Declaring generic functions
A generic function is declared using type parameter(s), which are enclosed in angle brackets < > before the function's name. The type parameters can then be used to specify the function's parameter types, return type, or both:
fun <T> foo(value: T): T {
return value
}
In this example, <T> declares a type parameter named T. The function foo takes an argument of type T and returns a value of the same type. This allows the function to operate on any type, making it generic.
Here's a more practical example:
fun <T> filterList(list: List<T>, predicate: (T) -> Boolean): List<T> {
val filtered = mutableListOf<T>()
for (element in list) if (predicate(element)) filtered.add(element)
return filtered
}
filterList takes a list of type T, applies a provided condition to filter its elements, and returns a new list with the elements that satisfy the condition.
It's important to understand the difference between declaring a type parameter and using that type parameter:
Declaration with
<T>: The<T>you see within angle brackets of a function or class is where you're declaring thatTis a type parameter. It's like saying, "I'm going to use a type, and I will refer to it asTfrom now on".Usage: When you use
Tinside the function's parameter types, return type, or body (for instance, as inList<T>), you're now using that type parameter. Here,Tis treated as an actual type. Therefore,TinList<T>is provided as a type argument.
To call this function we could write:
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = filterList<Int>(numbers) { it % 2 == 0 }
// Int explicitly specified as the type parameter
Admittedly, Kotlin's built-in filter function is cooler, but this will do for now, and we'll later improve it to make it closer to Kotlin's filter.
When calling a generic function, you can explicitly specify the type argument(s) by putting them in angle brackets, like the snippet above uses <Int>. However, the Kotlin compiler can often infer the type arguments based on the context. This means you can usually omit them:
val names = listOf("Alice", "Bob", "", "Charlie", "")
val nonEmptyNames = filterList(names) { it.isNotEmpty() }
// The type String is inferred from the 'names' list, as it contains String elements
Like generic classes, generic functions can have multiple type parameters. These are separated by commas within the angle brackets:
fun <T, U> reversePair(pair: Pair<T, U>): Pair<U, T> {
return Pair(pair.second, pair.first)
}
Kotlin 1.7.0 introduced the ability to use an underscore _ to automatically infer a type argument when other type arguments are explicitly specified. This can be helpful in situations where only one or some of the type arguments can be easily determined from the context, and you want to avoid repetition:
interface MyInterface<T>
fun <T, F : MyInterface<T>> foo() {}
fun main() {
foo<_, MyInterface<String>>() // The first type argument T will be inferred as String based on the second argument
}
In this example, the second type argument F is explicitly provided as MyInterface<String>, therefore the first type argument T is inferred as String.
Generic methods
Classes can also define generic methods, which are declared like regular generic functions and have access to other class members like any ordinary method. Let's take a look at an example with a non-generic class:
class Bar {
fun <T> foo(value: T): T {
// ...
return value
}
}
fun main() {
val bar = Bar()
val value = bar.foo("Hello!") // Type String inferred
println(value) // Output: Hello!
}
Generic methods are more versatile when used within generic classes. This enables methods to interact with the class's type parameters:
class Box<T> {
private var item: T? = null
fun put(item: T) {
this.item = item
}
fun get(): T? = item
fun <U> transformItem(transformer: (T) -> U): U? {
return item?.let(transformer)
}
}
In this example, the method transformItem uses both the class’s type parameter T and its own type parameter U.
Extension functions
Kotlin extension functions can be generic as well. Let’s improve our earlier filterList function by making it an extension function on List:
fun <T> List<T>.filterList(predicate: (T) -> Boolean): List<T> {
val filtered = mutableListOf<T>()
for (element in this) if (predicate(element)) filtered.add(element)
return filtered
}
Now, our filterList function is an extension function and can be used as follows:
val numbers = listOf(1, 2, 3, 4, 5)
val oddNumbers = numbers.filterList { it % 2 != 0 }
Our filterList function now feels more like Kotlin's filter. However, it's not an exact replica, as Kotlin's filter is more versatile, designed to work with more than just lists of various element types.
Type safety
Let's take a closer look at how generic functions help preserve type safety. Consider the following filterList function that uses Any instead of a type parameter:
fun List<Any>.filterList(predicate: (Any) -> Boolean): List<Any> {
val filtered = mutableListOf<Any>()
for (element in this) if (predicate(element)) filtered.add(element)
return filtered
}
Although this function works with lists containing different types of elements, it loses the original type information. For example, if you pass a List<String>, it returns a List<Any>, requiring you to cast it back to List<String>. As a result, the compiler cannot assist in catching type-related errors:
val scores = listOf(53, 67, 89, 99, 45, 78)
val passedScores = scores.filterList { it as Int >= 60 }
// passedScores will be of type List<Any>, requiring a cast to List<Int> for further use as a list of integers
val passedScoresAsInts = passedScores as List<Int>
Downcasting can lead to errors and compromises type safety, whereas generic functions allow you to preserve it.
Conclusion
Generic functions, like generic classes, are a key part of writing reusable code in Kotlin that also works with different types while preserving type safety. Generic functions use type parameters that are not specific to any type, thus promoting reuse. They are versatile and can be declared on their own, inside classes, and can be used as extension functions. While Kotlin supports implicit type arguments, explicit type arguments can be required in some cases where the type cannot be inferred. Using generics, you can maintain type safety while working with different types, and avoid unsafe operations such as casting which helps in reducing run time errors.