I'm trying to build a good mental model for lambdas with receivers in Kotlin, and how DSLs work. The simples ones are easy, but my mental model falls apart for the complex ones.
Say we have a function changeVolume
that looks like this:
fun changeVolume(operation: Int.() -> Int): Unit {
val volume = 10.operation()
}
The way I would describe this function out loud would be the following:
A function changeVolume takes a lambda that must be applicable to an Int (the receiver). This lambda takes no parameters and must return an Int. The lambda passed to changeVolume will be applied to the Int 10, as per the 10.lambdaPassedToFunction() expression.
I'd then invoke this function using something like the following, and all of a sudden we have the beginning of a small DSL:
changeVolume {
plus(100)
}
changeVolume {
times(2)
}
This makes a lot of sense because the lambda passed is directly applicable to any Int
, and our function simply makes use of that internally (say 10.plus(100)
, or 10.times(2)
)
But take a more complex example:
data class UserConfig(var age: Int = 0, var hasDog: Boolean = true)
val user1: UserConfig = UserConfig()
fun config(lambda: UserConfig.() -> Unit): Unit {
user1.lambda()
}
Here again we have what appears to be a simple function, which I'd be tempted to describe to a friend as "pass it a lambda that can have a UserConfig
type as a receiver and it will simply apply that lambda to user1
".
But note that we can pass seemingly very strange lambdas to that function, and they will work just fine:
config {
age = 42
hasDog = false
}
The call to config
above works fine, and will change both the age
and the hasDog
properties. Yet it's not a lambda that can be applied the way the function implies it (user1.lambda()
, i.e. there is no looping over the 2 lines in the lambda).
The official docs define those lambdas with receivers the following way: "The type A.(B) -> C
represents functions that can be called on a receiver object of A
with a parameter of B
and return a value of C
."
I understand that the age
and the hasDog
can be applied to the user1
individually, as in user1.age = 42
, and also that the syntactic sugar allows us to omit the this.age
and this.hasDog
in the lambda declaration. But how can I reconcile the syntax and the fact that both of those will be run, sequentially nonetheless! Nothing in the function declaration of config()
would lead me to believe that events on the user1
will be applied one by one.
Is that just "how it is", and sort of syntactic sugar and I should learn to read them as such (I mean I can see what it's doing, I just don't quite get it from the syntax), or is there more to it, as I imagine, and this all comes together in a beautiful way through some other magic I'm not quite seeing?
The lambda is like any other function. You aren't looping through it. You call it and it runs through its logic sequentially from the first line to a return statement (although a bare return
keyword is not allowed). The last expression of the lambda is treated as a return statement. If you had not defined your parameter as receiver, but instead as a standard parameter like this:
fun config(lambda: (UserConfig) -> Unit): Unit {
user1.lambda()
}
Then the equivalent of your above code would be
config { userConfig ->
userConfig.age = 42
userConfig.hasDog = false
}
You can also pass a function written with traditional syntax to this higher order function. Lambdas are only a different syntax for it.
fun changeAgeAndRemoveDog(userConfig: UserConfig): Unit {
userConfig.age = 42
userConfig.hasDog = false
}
config(::changeAgeAndRemoveDog) // equivalent to your lambda code
or
config(
fun (userConfig: UserConfig): Unit {
userConfig.age = 42
userConfig.hasDog = false
}
)
Or going back to your original example Part B, you can put any logic you want in the lambda because it's like any other function. You don't have to do anything with the receiver, or you can do all kinds of stuff with it, and unrelated stuff, too.
config {
age = 42
println(this) // prints the toString of the UserConfig receiver instance
repeat(3) { iteration ->
println(copy(age = iteration * 4)) // prints copies of receiver
}
(1..10).forEach {
println(it)
if (it == 5) {
println("5 is great!")
}
}
hasDog = false
println("I return Unit.")
}