1. Overview
In this article, we’re going to explore Lambdas in the Kotlin language. Keep in mind that lambdas aren’t unique to Kotlin and have been around for many years in many other languages.
Lambdas Expressions are essentially anonymous functions that we can treat as values – we can, for example, pass them as arguments to methods, return them, or do any other thing we could do with a normal object.
2. Defining a Lambda
As we’ll see, Kotlin Lambdas are very similar to Java Lambdas. You can find out more about how to work with Java Lambdas and some best practices here.
To define a lambda, we need to stick to the syntax:
val lambdaName : Type = { argumentList -> codeBody }
The only part of a lambda that isn’t optional is the codeBody.
The argument list can be skipped when defining at most one argument and the Type can often be inferred by the Kotlin compiler. We don’t always need a variable as well, the lambda can be passed directly as a method argument.
The type of the last command within a lambda block is the returned type.
2.1. Type Inference
Kotlin’s type inference allows the type of a lambda to be evaluated by the compiler.
Writing a lambda that produces the square of a number would be as written as:
val square = { number: Int -> number * number }
val nine = square(3)
Kotlin will evaluate the above example to be a function that takes one Int and returns an Int: (Int) -> Int
If we wanted to create a lambda that multiplies its single argument numbers by 100 then returns that value as a String:
val magnitude100String = { input : Int ->
val magnitude = input * 100
magnitude.toString()
}
Kotlin will understand that this lambda is of type (Int) -> String.
2.2. Type Declaration
Occasionally Kotlin can’t infer our types and we must explicitly declare the type for our lambda; just as we can with any other type.
The pattern is input -> output, however, if the code returns no value we use the type Unit:
val that : (Int) -> Int = { three -> three }
val more : (String, Int) -> String = { str, int -> str + int }
val noReturn : (Int) -> Unit = { num -> println(num) }
We can use lambdas as class extensions:
val another : String.(Int) -> String = { this + it }
The pattern we use here is slightly different from the other lambdas we have defined. Our brackets still contain our arguments but before our brackets, we have the type that we’re going to attach this lambda to.
To use this pattern from a String we call the *Type.lambdaName(arguments)*so to call our ‘another’ example:
fun extendString(arg: String, num: Int) : String {
val another : String.(Int) -> String = { this + it }
return arg.another(num)
}
2.3. Returning from a Lambda
The final expression is the value that will be returned after a lambda is executed:
val calculateGrade = { grade : Int ->
when(grade) {
in 0..40 -> "Fail"
in 41..70 -> "Pass"
in 71..100 -> "Distinction"
else -> false
}
}
The final way is to leverage the anonymous function definition – we must define the arguments and return type explicitly and may use the return statement the same as any method:
val calculateGrade = fun(grade: Int): String {
if (grade < 0 || grade > 100) {
return "Error"
} else if (grade < 40) {
return "Fail"
} else if (grade < 70) {
return "Pass"
}
return "Distinction"
}
3. it
A shorthand of a single argument lambda is to use the keyword ‘it’. This value represents any lone that argument we pass to the lambda function.
We’ll perform the same forEach method on the following array of Ints:
val array = arrayOf(1, 2, 3, 4, 5, 6)
We’ll first look at the longhand form of the lambda function, followed by the shorthand form of the same code, where ‘it’ will represent each element in the following array.
Longhand:
array.forEach { item -> println(item * 4) }
Shorthand:
array.forEach { println(it * 4) }
4. Implementing Lambdas
We’ll very briefly cover how to call a lambda that is in scope as well as how to pass a lambda as an argument.
Once a lambda object is in scope, call it as any other in-scope method, using its name followed by brackets and any arguments:
fun invokeLambda(lambda: (Double) -> Boolean) : Boolean {
return lambda(4.329)
}
If we need to pass a lambda as an argument into a higher-order method, we have five options.
4.1. Lambda Object Variable
Using an existing lambda object as declared in section 2, we pass the object into the method as we’d with any other argument:
@Test
fun whenPassingALambdaObject_thenCallTriggerLambda() {
val lambda = { arg: Double ->
arg == 4.329
}
val result = invokeLambda(lambda)
assertTrue(result)
}
4.2. Lambda Literal
Instead of assigning the lambda to a variable, we can pass the literal directly into the method call:
Test
fun whenPassingALambdaLiteral_thenCallTriggerLambda() {
val result = invokeLambda({
true
})
assertTrue(result)
}
4.3. Lambda Literal Outside the Brackets
Another pattern for lambda literals encouraged by JetBrains – is to pass the lambda in as the last argument to a method and place the lambda outside the method call:
@Test
fun whenPassingALambdaLiteralOutsideBrackets_thenCallTriggerLambda() {
val result = invokeLambda { arg -> arg.isNaN() }
assertFalse(result)
}
4.4. Method References
Finally, we have the option of using method references. These are references to existing methods.
In our example below, we take Double::isFinite. That function then takes on the same structure as a lambda, however, it’s of type KFunction1<Double, Boolean> as it has one argument, takes in a Double and returns a Boolean:
@Test
fun whenPassingAFunctionReference_thenCallTriggerLambda() {
val reference = Double::isFinite
val result = invokeLambda(reference)
assertTrue(result)
}
5. Kotlin Lambda in Java
Kotlin uses generated function interfaces to interop with Java. They exist in the Kotlin source code here.
We have a limit on the number of arguments that can be passed in with these generated classes. The current limit is 22; represented by the interface Function22.
The structure of a Function interface’s generics is that the number and represents the number of arguments to the lambda, then that number of classes will be the argument Types in order.
The final generic argument is the return type:
import kotlin.jvm.functions.*
public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R
}
When there is no return type defined within the Kotlin code, then the lambda returns a Kotlin Unit. The Java code must import the class from the kotlin package and return with null.
Below is an example of calling a Kotlin Lambda from a project that is part Kotlin and part Java:
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
...
new Function1<Customer, Unit>() {
@Override
public Unit invoke(Customer c) {
AnalyticsManager.trackFacebookLogin(c.getCreated());
return null;
}
}
When using Java8, we use a Java lambda instead of a Function anonymous class:
@Test
void givenJava8_whenUsingLambda_thenReturnLambdaResult() {
assertTrue(LambdaKt.takeLambda(c -> c >= 0));
}
6. Anonymous Inner Classes
Kotlin has two interesting ways of working with Anonymous Inner Classes.
6.1. Object Expression
When calling a Kotlin Inner Anonymous Class or a Java Anonymous Class comprised of multiple methods we must implement an Object Expression.
To demonstrate this, we’ll take a simple interface and a class that takes an implementation of that interface and calls the methods depend on a Boolean argument:
class Processor {
interface ActionCallback {
fun success() : String
fun failure() : String
}
fun performEvent(decision: Boolean, callback : ActionCallback) : String {
return if(decision) {
callback.success()
} else {
callback.failure()
}
}
}
Now to provide an anonymous inner class, we need to use the “object” syntax:
@Test
fun givenMultipleMethods_whenCallingAnonymousFunction_thenTriggerSuccess() {
val result = Processor().performEvent(true, object : Processor.ActionCallback {
override fun success() = "Success"
override fun failure() = "Failure"
})
assertEquals("Success", result)
}
6.2. Lambda Expression
On the other hand, we may also have the option of using a lambda instead. Using lambdas in lieu of an Anonymous Inner Class has certain conditions:
- The class is an implementation of a Java interface (not a Kotlin one)
- the interface must have a max
If both of these conditions are met, we may use a lambda expression instead.
The lambda itself will take as many arguments as the interface’s single method does.
A common example would be using a lambda instead of a standard Java Consumer:
val list = ArrayList<Int>(2)
list.stream()
.forEach({ i -> println(i) })
7. Conclusion
While syntactically similar, Kotlin and Java lambdas are completely different features. When targeting Java 6, Kotlin must transform its lambdas into a structure that can be utilized within JVM 1.6.
Despite this, the best practices of Java 8 lambdas still apply.
More about lambda best practices here.
Code snippets, as always, can be found over on GitHub.