Skip to content

Kotlin Primer

Lambdas

A SAM (Single-Abstract Method) Interface represents a method/function that we're passing around. We're really interested in that method/function; the interface is just a carrier, and many languages have ways to just specify the method/function directly, by itself. Let's work our way toward that...

Java Lambdas

In Java, the only way to specify a method is inside a type (interface, class, or enumeration type). It's clunky, but it works. If we're specifying a SAM Interface, we can annotate it using @FunctionalInterface to tell others that it should only ever have a single method (and enforce that at compile time).

@FunctionalInterface
interface OnClickListener {
    void onClick(Button button);
}

Instead of specifying an explicit or anonymous-inner class, we can use a Lambda, a (very) minimal function definition that meets the required SAM.

public class JavaMain {
    static void main() {
        Button button = new Button();
        button.setOnClickListener((clickedButton) -> {
            System.out.println("Button Clicked!");
        });
    }
}

The syntax (parameters) -> { ... } defines the lambda. The number of parameters must match the expected SAM, and are matched positionally. Each parameter is assigned a value by the caller, and passed into the body, where it can be used. In this example, we're not using the parameter.

Note

In this example, we could shorten the lambda to

(clickedButton) -> System.out.println("Button Clicked!")

because the body is a single expression.

Lambdas in Java are syntactic sugar for anonymous inner classes, and have all the same limitations. In many languages, lambdas are true closures, which means they retain access to their surrounding context. If variables in that surrounding context change, for example, and the lambda runs later, it will see the new values of those variables! Nifty!

Instead, Java captures the values of any surrounding values it uses, and all such surrounding values must be "effectively final". This means the variables must be final, or must never change after the lambda definition.

Kotlin Lambdas

Kotlin lambdas, on the other hand, are closures. Yay! But let's not get ahead of ourselves.

We can keep using a SAM Interface (don't... just... don't) by tweaking it with the fun keyword, and then define a Kotlin lambda for the implementation

fun interface OnClickListener {
    fun onClick(button: Button)
}

fun main() {
    val button = Button()
    button.onClickListener = { button -> println("Button Clicked!") }
}

Kotlin lambdas have a slightly different syntax. { params -> body }, and there are a few interesting syntactic helpers.

If you don't use a parameter, you can change it to _ to make it clear that you don't intend to use it. This example is perfect for that:

button.onClickListener = { _ -> println("Button Clicked!") }

This tells the reader there is a single parameter that you're not using. This could occur with multiple parameters as well:

val someLambda = { a, _, c -> println("a=$a, c=$c") }

If the SAM only requires a single parameter, you can either specify it by name:

val someLambda = { name -> println("name=$name") }

or use it as an implicit name for the parameter:

val someLambda = { println("name=$it") }

You never have to use it, but sometimes the lambda is so simple, it is very obvious.

I mentioned earlier that you shouldn't define SAM interfaces in Kotlin. So what should you do?

Function Types

Why specify an entire interface just to define what function we want? What do we really need to define a function?

The identifying signature of a function includes:

  • function name
  • parameter types
  • return type

When compiling your code, the names of the parameters don't contribute to its uniqueness (nor does the return type), just their types. So functions

fun foo(a: Int): Int {...}
fun foo(b: Int): Int {...}
fun foo(b: Int): String {...}

have the same signature, and are considered "conflicting overloads". This is required so when they are called, the function name and type of the arguments is enough to resolve the call.

The name and parameter types are enough. We can use this to create a "function type" rather than a SAM interface. Suppose we wanted to define a function that took two Ints and returned an Int:

fun main() {
    var mathy: (Int, Int) -> Int = { a, b -> a + b }
    println(mathy(10, 2))
    mathy = { a, b -> a - b }
    println(mathy(10, 2))
}

The (Int, Int) -> Int defines the two Int parameters and the Int return type. We can assign properties of this type to a lambda.

Function References

But what if we have an existing function that we want to use? We can reference those functions with the :: operator:

class Calculator {
    fun add(a: Int, b: Int) = a + b
    fun subtract(a: Int, b: Int) = a - b
}

fun add1(a: Int, b: Int) = a + b
fun subtract1(a: Int, b: Int) = a - b

fun main() {
    fun add2(a: Int, b: Int) = a + b
    fun subtract2(a: Int, b: Int) = a - b

    // references to top-level functions
    var mathy: (Int, Int) -> Int = ::add1
    println(mathy(10, 2))
    mathy = ::subtract1
    println(mathy(10, 2))

    // references to nested functions
    mathy = ::add2
    println(mathy(10, 2))
    mathy = ::subtract2
    println(mathy(10, 2))

    // references in a class instance
    val calculator = Calculator()
    mathy = calculator::add
    println(mathy(10, 2))
    mathy = calculator::subtract
    println(mathy(10, 2))
}

This allows us to reference top-level, nested, or member functions in a class instance. We'll be using these a lot!

Nullable Function Types

When you want to make a function type by nullable, be careful! If you specify

val mathy: (Int, Int) -> Int?

you're actually saying the return type is nullable, not the overall function type! To properly specify the function is nullable:

val mathy: ((Int, Int) -> Int)? = null

No Return Type

But what if the function doesn't have a return type?

fun foo(a: Int) {
    println(a)
}

How do we represent this?

The trick is knowing how Kotlin represents the function. There's a special type, Unit that is the return type for functions that don't return anything. The above function is equivalent to

fun foo(a: Int): Unit {
    println(a)
}

You can specify this if you'd really like, but it's common style not to.

Now the function type for foo becomes obvious:

(Int) -> Unit

and it's quite common to see the same with no parameters:

() -> Unit

The Kotlin Observer

Now let's finish off the Kotlin version of the Observer.

class Button {
    var onClickListener: ((Button) -> Unit)? = null

    private fun internalClickLogic() {
        // watches for gestures that indicate a click on this button
        // when clicked:
        val listener = onClickListener
        if (listener != null) {
            listener(this)
        }
        // or to use the shorter, safe-access operator, we explicitly call invoke
        onClickListener?.invoke(this) // inform it we were clicked

    }
}

fun main() {
    val button = Button()
    button.onClickListener = { println("Button Clicked!") }
}

We define onClickListener as a nullable function that takes a Button and doesn't return anything.

Because it's nullable, we must check to see if it's null before we use it. We can use the capture-and-check approach:

val listener = onClickListener
if (listener != null) {
    listener(this)
}

and call it exactly like we would a function. The listener(this) actually calls a function called invoke behind the scenes. We can use this knowledge to explicitly call invoke with a safe-access operator:

onClickListener?.invoke(this)