Kotlin, a modern and versatile programming language, offers various features to enhance developer productivity. One such powerful feature is function types, which enable you to treat functions as first-class citizens in your code. In this blog post, we will delve into function types in Kotlin, understand their types, explore their usage, and provide examples to solidify our understanding. So let’s dive in!
Recap: Higher-Order Functions
In Kotlin, a higher-order function is a function that can accept a lambda expression or a function reference as an argument or can return a lambda expression or a function reference. It allows functions to be treated as values and enables flexible and concise coding.
Let’s take the example of the filter
function from the standard library, which takes a predicate function as an argument and is, therefore, a higher-order function:
list.filter { x > 0 }
Function types
In Kotlin, you can declare variables with function types. This means that the variables can hold references to functions. Let’s take a look at an example:
val sum: (Int, Int) -> Int = { x, y -> x + y } // Function that takes two Int parameters and returns an Int value
val action: () -> Unit = { println(42) } // Function that takes no arguments and doesn’t return a value
In this code, we have two variables: sum
and action
. The type of sum
is a function that takes two Int
parameters and returns an Int
. The type of action
is a function that takes no parameters and returns Unit
, which represents a lack of meaningful value.
What are Function Types?
Function types allow you to treat functions as values. Just like any other variable, you can assign functions to variables, pass them as parameters to other functions, and even return them from functions. This feature provides flexibility and enables you to write more concise and expressive code.
Syntax
To declare a function type, you put the function parameter types in parentheses, followed by an arrow, and the return type of the function. In the above diagram, (Int, String) -> Unit
specifies a function that takes Int
and String
parameters and returns a Unit
.
The Unit
type is used to indicate that a function doesn’t return a meaningful value. In regular function declarations, you can omit the Unit
return type, but in function type declarations, it is always required. So, you can’t omit a Unit
in this context.
val sum: (Int, Int) -> Int = { x, y -> x + y }
In the lambda expression { x, y -> x + y }
, you might notice that the types of the parameters x
and y
are omitted. This is because the types are already specified in the function type declaration, so there’s no need to repeat them in the lambda itself.
You can also make the return type of a function type nullable by using the?
symbol. For example:
var canReturnNull: (Int, Int) -> Int? = { null }
In this case, the function type (Int, Int) -> Int?
represents a function that takes two Int
parameters and returns an Int
that can be nullable. This means the function can return either an Int
value or null
.
Furthermore, you can declare a nullable variable of a function type by enclosing the entire function type definition in parentheses and placing the question mark after the parentheses. For example:
var funOrNull: ((Int, Int) -> Int)? = null
Here, ((Int, Int) -> Int)?
represents a nullable variable of a function type. The entire function type definition is enclosed in parentheses, and the question mark indicates that the variable itself is nullable.
It’s important to note the distinction between a function type with a nullable return type (
(Int, Int) -> Int?
) and a nullable variable of a function type (((Int, Int) -> Int)?
). Omitting the parentheses will result in different meanings, so be cautious when specifying nullable function types.
Parameter names of function types
In Kotlin, you have the option to specify names for the parameters of a function type. This can improve the readability of your code and can be helpful for code completion in the IDE.
Here’s an example that demonstrates specifying parameter names in a function type:
fun performRequest(url: String, callback: (code: Int, content: String) -> Unit) {
/* ... */
}
In this code, the performRequest
function takes two parameters: url
of type String
and callback
of type (code: Int, content: String) -> Unit
. The callback
parameter is a function type that expects two parameters named code
and content
, both of type Int
and String
respectively. The function type represents a callback function that will be invoked when the request is completed.
When you call the performRequest
function, you can provide a lambda expression as the argument for the callback
parameter. The lambda can use any parameter names you prefer, regardless of the names specified in the function type declaration. For example:
val url = "https://blog.softaai.com"
performRequest(url) { code, content ->
// Code that uses the parameters 'code' and 'content'
}
performRequest(url) { code, page ->
// Code that uses the parameters 'code' and 'page'
}
In the above examples, we pass a lambda expression to the performRequest
function. Inside the lambda, we can choose different names for the parameters (code
and content
in the first example, and code
and page
in the second example). These parameter names in the lambda expression do not need to match the names specified in the function type declaration.
Although the parameter names don’t affect type matching, using descriptive names can make your code more readable and understandable. Additionally, modern IDEs can utilize these parameter names for code completion, making it easier for you to write your code accurately.
Calling functions passed as arguments
In Kotlin, you can call functions that are passed as arguments to other functions. Let’s explore a couple of examples to understand how this works.
First, let’s consider the twoAndThree
function, which takes another function as an argument and performs an arbitrary operation on the numbers 2 and 3:
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3)
println("The result is $result")
}
In this example, the twoAndThree
function accepts a function called operation
, which has a function type (Int, Int) -> Int
. This means the operation
function takes two Int
parameters and returns an Int
. Inside the twoAndThree
function, the operation
function is called with arguments 2 and 3, and the result is printed.
To call the twoAndThree
function and pass a function as an argument, you can use a lambda expression. For example:
twoAndThree { a, b -> a + b }
In this case, we pass a lambda expression that adds two numbers (a + b
). The lambda matches the function type (Int, Int) -> Int
because it takes two Int
parameters and returns an Int
. The result of the addition, 5, is printed by the twoAndThree
function.
Similarly, you can pass a different lambda expression to achieve a different operation:
twoAndThree { a, b -> a * b }
Here, the lambda multiplies the two numbers (a * b
), and the result, 6, is printed.
The syntax for calling a function passed as an argument is the same as calling a regular function. You use parentheses after the function name and provide the necessary arguments inside the parentheses.
Now, let’s consider another example: reimplementing the filter
function from the standard library. The filter
function takes a predicate as a parameter. The predicate is a function that takes a character and returns a Boolean result. The implementation checks whether each character satisfies the predicate and adds it to a StringBuilder
if it does.
To reimplement the filter
function for strings, let’s consider the following implementation:
fun String.filter(predicate: (Char) -> Boolean): String {
val sb = StringBuilder()
for (index in 0 until length) {
val element = get(index)
if (predicate(element))
sb.append(element)
}
return sb.toString()
}
In this implementation, the filter
function is an extension function on the String
class. It takes a predicate
parameter, which is a function that takes a Char
parameter and returns a Boolean
.
Inside the function, a StringBuilder
is created to store the filtered characters. The function iterates over each character of the string using the for
loop and checks if the character satisfies the given predicate. If the predicate returns true
for a character, it is appended to the StringBuilder
.
Finally, the StringBuilder
is converted to a string using the toString()
function and returned as the result.
Here’s an example of how you can use the filter
function:
println("softAai Apps".filter { it in 'A'..'Z' })
In this case, the input string is "softAai Apps"
, and the predicate checks if each character is within the range from 'A'
to 'Z'
. The filtered result, which only contains the uppercase alphabetic characters, is printed as "AA"
.
The filter
function implementation is straightforward. It enables you to filter characters from a string based on a given predicate, providing a more convenient and readable way to perform such operations.
By using higher-order functions and passing functions as arguments, you can create flexible and reusable code that can perform different operations based on the provided functions.
Default and null values for parameters with function types
When declaring a parameter of a function type, you can specify a default value for it. This can be useful when you want to provide a default behavior for the function if the caller doesn’t provide a specific implementation. Here’s an example:
fun <T> printCollection(collection: Collection<T>, transform: (T) -> String = { it.toString() }) {
for (element in collection) {
println(transform(element))
}
}
In this example, the printCollection
function takes a collection
parameter of type Collection<T>
and a transform
parameter of function type (T) -> String
. The transform
parameter has a default value defined as a lambda expression { it.toString() }
, which uses the toString()
method to convert each element of the collection to a string.
You can call the printCollection
function in different ways:
val numbers = listOf(1, 2, 3, 4, 5)
// Omitting the transform parameter to use the default behavior
printCollection(numbers)
// Passing a lambda as the transform parameter
printCollection(numbers) { "Number: $it" }
// Passing the transform parameter as a named argument
printCollection(numbers, transform = { "Value: $it" })
In the first example, we omit the transform
parameter, so the default behavior using toString()
will be used to convert each element.
In the second example, we pass a lambda expression { "Number: $it" }
as the transform
parameter. This lambda defines a custom behavior to transform each element of the collection.
In the third example, we explicitly pass the transform
parameter as a named argument, providing a different lambda expression { "Value: $it" }
to customize the transformation.
Another option is to declare a parameter of a nullable function type. However, directly calling a function passed in such a parameter is not allowed because it could potentially lead to null pointer exceptions. To handle this, you can check for null explicitly or use the safe-call syntax callback?.invoke()
. Here’s an example:
fun performAction(callback: (() -> Unit)?) {
callback?.invoke()
}
In this example, the performAction
function takes a nullable function type parameter callback
of type (() -> Unit)?
. Inside the function, we can use the safe-call syntax callback?.invoke()
to invoke the function only if it is not null.
Function Types with Generic Parameters
Kotlin allows you to define function types with generic parameters. This provides flexibility when working with functions that can operate on different types.
fun <T> processList(list: List<T>, operation: (T) -> Unit) {
for (item in list) {
operation(item)
}
}
val numbers = listOf(1, 2, 3, 4, 5)
processList(numbers) { println(it) } // Prints each number in the list
In the above example, the processList
function takes a list of generic type T
and a function type (T) -> Unit
as parameters. The operation
function is called for each item in the list and can perform any desired operation.
Now you know how to write functions that take functions as arguments, including how to provide default values for function parameters and handle nullable function types. This allows you to create more flexible and customizable functions in Kotlin.
Returning functions from functions
Returning functions from functions allows you to dynamically choose and provide different logic based on certain conditions or states. This can be useful in scenarios where the behavior of a program needs to adapt to different situations.
For example, let’s consider a shipping cost calculation scenario. The shipping cost may vary depending on the chosen delivery method. We can define a function called getShippingCostCalculator
that takes the delivery
parameter of type Delivery
(an enum class representing different delivery options) and returns a function of type (Order) -> Double
. The returned function calculates the shipping cost based on the selected delivery method.
Here’s an example implementation:
enum class Delivery { STANDARD, EXPEDITED }
class Order(val itemCount: Int)
fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double { // Declares a function that returns a function
if (delivery == Delivery.EXPEDITED) {
return { order -> 6 + 2.1 * order.itemCount } // Returns lambdas from the function
}
return { order -> 1.2 * order.itemCount } // Returns lambdas from the function
}
In this example, when getShippingCostCalculator
is called with Delivery.EXPEDITED
, it returns a function that takes an Order
parameter and calculates the shipping cost as 6 + 2.1 * order.itemCount
. For any other delivery option, it returns a different function that calculates the shipping cost as 1.2 * order.itemCount
.
You can use the returned function to calculate the shipping cost for a specific order. Here’s an example of usage:
val calculator = getShippingCostCalculator(Delivery.EXPEDITED) // Stores the returned function in a variable
println("Shipping costs ${calculator(Order(3))}") // Invokes the returned function
In this code, we obtain the calculator function by calling getShippingCostCalculator
with Delivery.EXPEDITED
. Then, we pass an Order
object with itemCount
as 3 to the calculator
function, which calculates and returns the shipping cost as 12.3
. Finally, we print the shipping cost.
Let’s consider one more scenario where returning functions from functions is useful.
Suppose you’re working on a GUI contact-management application, and you need to determine which contacts should be displayed based on the state of the user interface (UI). You can define a function called getContactFilter
that takes a UI state as a parameter and returns a function of type (Contact) -> Boolean
. The returned function will determine whether a contact should be displayed or not based on the UI state.
Here’s an example implementation:
data class Contact(val name: String, val isFavorite: Boolean)
enum class UIState { ALL, FAVORITES }
fun getContactFilter(uiState: UIState): (Contact) -> Boolean { // Declares a function that returns a function
return when (uiState) {
UIState.ALL -> { true } // Display all contacts
UIState.FAVORITES -> { contact -> contact.isFavorite } // Display only favorite contacts
}
}
In this example, getContactFilter
takes a UIState
parameter and returns a function of type (Contact) -> Boolean
. The returned function determines whether a contact should be displayed or not based on the UI state. If the UI state is UIState.ALL
, the returned function always returns true
, indicating that all contacts should be displayed. If the UI state is UIState.FAVORITES
, the returned function checks the isFavorite
property of the contact and returns its value, indicating whether the contact is a favorite or not.
You can use the returned function to filter the contacts based on the UI state. Here’s an example usage:
val filter = getContactFilter(UIState.FAVORITES)
val contacts = listOf(
Contact("amol pawar", true),
Contact("atul tele", false),
Contact("akshay pawal", true)
)
val filteredContacts = contacts.filter(filter)
println("Filtered Contacts:")
filteredContacts.forEach { println(it.name) }
In this code, we obtain the filter function by calling getContactFilter
with UIState.FAVORITES
. Then, we have a list of contacts, and we use the filter
function to filter the contacts based on the UI state. The filtered contacts, in this case, are the contacts that are marked as favorites. Finally, we print the names of the filtered contacts.
Returning functions from functions allows you to dynamically select and apply different logic based on conditions or states, providing flexibility and customization in your code.
Using function types from Java
Under the hood, function types are declared as regular interfaces: a variable of a function type is an implementation of a FunctionN interface. The Kotlin standard library defines a series of interfaces, corresponding to different numbers of function arguments: Function0 (this function takes no arguments), Function1 (this function takes one argument), and so on. Each interface defines a single invoke method, and calling it will execute the function. A variable of a function type is an instance of a class implementing the corresponding FunctionN interface, with the invoke method containing the body of the lambda.
Kotlin functions that use function types can be called easily from Java. Java 8 lambdas are automatically converted to values of function types:
/* Kotlin declaration */
fun processTheAnswer(f: (Int) -> Int) {
println(f(42))
}
// Java
processTheAnswer(number -> number + 1); // output : 43
In this case, the processTheAnswer
function in Kotlin takes a function type (Int) -> Int
as a parameter. In Java, you can pass a lambda expression number -> number + 1
as an argument, and it will be automatically converted to the corresponding function type.
What about older Java versions?
If you are using an older version of Java that doesn’t support lambdas, you can pass an instance of an anonymous class that implements the invoke
method from the corresponding function interface. Here’s an example:
// Java
processTheAnswer(new Function1<Integer, Integer>() { // Uses the Kotlin function type from Java code (prior to Java 8)
@Override
public Integer invoke(Integer number) {
System.out.println(number);
return number + 1;
}
});
In this Java example, we create an anonymous class that implements the Function1
interface. We override the invoke
method, which corresponds to the function body in Kotlin, and provide the desired implementation.
What about Extention Functions?
In Java, you can easily use extension functions from the Kotlin standard library that expect lambdas as arguments. Note, however, that they don’t look as nice as in Kotlin — you have to pass a receiver object as a first argument explicitly. Here’s an example:
// Java
List<String> strings = new ArrayList<>();
strings.add("42");
CollectionsKt.forEach(strings, s -> {
System.out.println(s);
return Unit.INSTANCE;
});
In this Java example, we use the CollectionsKt.forEach
extension function from the Kotlin standard library. We pass a lambda expression s -> { ... }
as an argument. Inside the lambda, we can perform the desired operations. Note that in Java, you need to explicitly return Unit.INSTANCE
to match the Kotlin requirement of returning Unit
.
It’s important to remember that in Java, functions or lambdas can return
Unit
. However, sinceUnit
has a value in Kotlin, you need to explicitly return it. Additionally, you cannot directly pass a lambda that returnsvoid
as an argument of a function type that expectsUnit
as the return type.
Overall, while using function types and lambdas from Kotlin in Java may require some adjustments in syntax, it is still possible to utilize Kotlin’s higher-order functions and achieve the desired functionality.
Removing duplication through lambdas
In Kotlin, lambdas are anonymous functions that can be treated as values. They allow you to define blocks of code that can be passed around and executed later. This flexibility enables us to write more concise and reusable code.
Suppose you have a scenario where you need to perform similar operations on different elements of a collection. Without lambdas, you might end up writing repetitive code for each element, resulting in duplication. However, by utilizing lambdas, you can extract the common behavior and eliminate duplication.
To illustrate this concept, let’s consider an example involving website visit data. We have a class called SiteVisit
that represents a visit to a website. Each visit has properties like path
(the visited URL), duration
(time spent on the page), and os
(operating system used by the visitor). The os
property is an enum called OS
, representing different operating systems.
data class SiteVisit(
val path: String,
val duration: Double,
val os: OS
)
enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
Now, let’s say we have a list of SiteVisit
objects called log
, which contains information about various visits to the website.
val log = listOf(
SiteVisit("/", 34.0, OS.WINDOWS),
SiteVisit("/", 22.0, OS.MAC),
SiteVisit("/login", 12.0, OS.WINDOWS),
SiteVisit("/signup", 8.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID)
)
Our goal is to calculate the average duration of visits from Windows machines. We can achieve this by filtering the visits based on the operating system, mapping the durations, and then calculating the average using the average
function.
val averageWindowsDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()
println(averageWindowsDuration) // Output: 23.0
In this example, we used the lambda expression { it.os == OS.WINDOWS }
as a filter to select only the visits with the Windows operating system. Then, we used the map
function to extract the durations of those visits. Finally, the average
function calculated the average duration.
Now, let’s say we want to calculate the average duration for visits from Mac users as well. Without using lambdas, we would need to write similar code again, resulting in duplication. However, we can avoid this duplication by extracting the common behavior into a function and parameterizing it.
We define an extension function called averageDurationFor
on the List<SiteVisit>
class, which takes an os
parameter representing the operating system. Inside the function, we filter the visits based on the given operating system, map the durations, and calculate the average.
fun List<SiteVisit>.averageDurationFor(os: OS) =
filter { it.os == os }
.map(SiteVisit::duration)
.average()
With this function in place, we can now calculate the average duration for both Windows and Mac visits without duplicating the code.
println(log.averageDurationFor(OS.WINDOWS)) // Output: 23.0
println(log.averageDurationFor(OS.MAC)) // Output: 22.0
By parameterizing the behavior that varies (in this case, the operating system), we eliminated duplication and made the code more reusable and maintainable.
However, there are situations where a simple parameter is not sufficient to capture the complexity of the condition we want to apply. For example, if we want to calculate the average duration for visits from mobile platforms (iOS and Android), a single parameter representing the operating system won’t be enough. In such cases, lambdas provide a powerful solution.
We can modify our averageDurationFor
function to take a lambda expression as a parameter. This lambda expression represents a condition that needs to be fulfilled for a visit to be included in the calculation.
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
filter(predicate)
.map(SiteVisit::duration)
.average()
Now, we can use this enhanced averageDurationFor
function to calculate the average duration based on more complex conditions. For example, finding the average duration for visits from Android and iOS users:
println(log.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) }) // Output: 12.15
In this case, we passed a lambda expression { it.os in setOf(OS.ANDROID, OS.IOS) }
to the averageDurationFor
function. This lambda expression represents the condition that checks if the operating system is either Android or iOS.
Similarly, we can use Lambdas to perform more intricate queries, such as finding the average duration of visits to the signup page from iOS users:
println(log.averageDurationFor { it.os == OS.IOS && it.path == "/signup" }) // Output: 8.0
Here, we provided a lambda expression { it.os == OS.IOS && it.path == "/signup" }
to specify the condition that includes only the visits from iOS users to the “/signup” page.
By using lambdas and function types, we can eliminate code duplication and extract both the repeated data and behavior into reusable functions. Lambdas allow us to write more expressive and concise code, making our programs easier to understand and maintain.
Function types and lambdas not only help eliminate code duplication but also provide a flexible and concise way to define different strategies or behaviors within your code. Instead of creating multiple classes or interfaces for each strategy, you can directly pass lambda expressions as different strategies, simplifying your code and making it more expressive.
Conclusion
Function types in Kotlin provide a powerful way to work with functions as first-class citizens. They enable you to pass functions as parameters, return them from functions, and even assign them to variables. This flexibility allows for concise and expressive code, making Kotlin a great language for functional programming paradigms. By understanding the various aspects of function types and exploring practical examples, you can leverage this feature to write more efficient and maintainable code. So go ahead, harness the power of function types in Kotlin, and take your programming skills to the next level!