Here’s the general syntax for defining a lambda expression and assigning it to a variable:

val lambdaName: (InputType) -> ReturnType = { input -> 
    // lambda body
}

Let’s put this into a concrete example. Suppose we have a list of integers and we want to define a lambda that checks if a number is positive. We could write:

val isPositive: (Int) -> Boolean = { number ->
    number > 0
}

In this example, isPositive is a lambda expression that takes an Int as input and returns a Boolean, indicating whether the input number is greater than zero. We’ve explicitly defined the input type (Int) and the return type Boolean of the lambda.

Once we’ve defined this lambda, we can use it as an argument to higher-order functions such as filter:

val numbers = listOf(-2, -1, 0, 1, 2)
val positiveNumbers = numbers.filter(isPositive)

Here, filter uses the isPositive lambda to filter out only the positive numbers from the list.

If our lambda doesn’t return any value (i.e., returns Unit), or if the context makes the types clear, we can omit the type annotations:

val printMessage = { message: String ->
    println(message)
}

When it is the Only Parameter

In Kotlin, it is a keyword used in lambda expressions to refer to the parameter implicitly when there’s only one parameter and it hasn’t been declared explicitly.

Here’s a basic example to illustrate the use of it:

val numbers = listOf(1, 2, 3)
numbers.forEach { println(it) }

In this example, forEach is a higher-order function that takes a lambda expression as its parameter. The lambda { println(it) } is executed for each element in the numbers list. Since there’s only one parameter to the lambda and its type can be inferred, we use it to refer to each element as it’s iterated over.

Kotlin lambdas allow for more complex operations too, not just iteration. We can use them for filtering, mapping, and aggregating data in collections. Here’s a slightly more complex example using filter and map:

val doubledPositiveNumbers = numbers.filter { it > 0 }.map { it * 2 }

In this case, filter removes any numbers that don’t satisfy the condition { it > 0 }, and map then takes each remaining number, referred to as it, and doubles it. The result is a new list assigned to doubledPositiveNumbers.

Optional Parentheses for the Lambda Argument

When we’re calling a function with a lambda expression as the last parameter, we have the option to place the lambda outside the parentheses. This convention is particularly common with higher-order functions, like filter and map, which take functions as parameters.

So, when we see:

numbers.filter { it > 0 }

It is equivalent to:

numbers.filter({ it > 0 })

This ability to move the lambda outside of the parentheses is particularly handy when the lambda expression is large, making the code easier to read and understand. Here’s a more complex example to illustrate:

val result = list.filter { it > 0 }
                 .map { 
                     println("Mapping $it")
                     it * 2 
                 }

Multiple Parameters

Lambdas in Kotlin can also have multiple parameters. In such cases, we can’t use it, and we need to declare each parameter explicitly, enclosed in parentheses. For example:

val sum = listOf(1 to 2, 3 to 4).map { (a, b) -> a + b }

Here, map is applied to a list of pairs, and each pair is destructured into a and b which are then summed.