Help with generics and lambda

I have this code given in a tutorial book on a chapter on generics:

class LootBox<T> (item: T) {
    var open = false
    private var loot: T = item

    fun fetch(): T? {
        return loot.takeIf { open }
    }

    fun <R> fetch(lootModFunction: (T) -> R): R? {
        return lootModFunction(loot).takeIf { open }
    }
}

class Fedora (val name: String, val value: Int)

class Coin (val value: Int)

fun main() {
    val lootBoxOne = LootBox(Fedora("a generic-looking fedora", 15))
    val lootBoxTwo = LootBox(Coin(15))

    lootBoxOne.open = true
    lootBoxOne.fetch()?.run {
        println("You retrieve ${this.name} from the box!")
    }
    
    val coin = lootBoxOne.fetch() {
        Coin(it.value * 3)
    }
    coin?.let {println(it.value)}
}
  1. First of all, how can we have 2 different functions with the same name? Is it allowed because they have different signatures? One fetch() function that doesn’t have any parameters, and one fetch() that does?

  2. Second of all, what’s the point of the variable “item” in the primary constructor, and then immediately reassigning it into a new variable, “loot”? Is that just standard procedure for generic constructors? Why not just use class LootBox<T> (private var loot: T)

  3. Third, and most important question, what is going on with the lambda parameter of the fetch function here? I don’t understand how this is working. I can understand that the fetch function is setting up a parameter named lootModFunction of type (T) -> R. So, when the function is called, it needs an argument that will pass an object of type T (which same as the type the function is called on) and returns a different type, R. So how does Coin(it.value * 3) function as (T) -> R?

  1. yes it’s called overloading.
  2. There’s no reason not to do what you say. Also loot is never reassigned it can be a val.
  3. { Coin(it.value * 3) } is a function which takes a Coin and returns a Coin worth 3 times as much.
    That lambda takes a Coin since that’s the type parameter of lootBoxOne and returns a Coin the compiler can and will deduce all the types for you.
1 Like

Thank you so much for the reply!

If I may, I still have some questions about #3–the use of that particular lambda in the generic class.
So, to backtrack a bit, here is another piece of code from the same book:

fun main() {
    val greetingFunction = { playerName: String, numBuildings: Int ->
        val currentYear = 2018
        println("Adding $numBuildings houses")
        "Welcome to SimVillage, $playerName! (copyright $currentYear)"
    }
    runSimulation("Guyal", greetingFunction)
}

fun runSimulation(playerName: String, greetingFunction: (String, Int) -> String) {
    val numBuildings = (1..3).shuffled().last()
    println(greetingFunction(playerName, numBuildings))
}

This example doesn’t use classes, but I can clearly see how the lambda is being used. The lambda, stored in a variable (greetingFunction), takes a string and an int and returns a string. So the parameter type defined in the runSimulation function is given as (String, Int) -> String.
The lambda/anonymous function is finally used when it is called in the runSimulation function and passed playerName (a string) and numBuildings (an int). The lambda defines how those arguments are used and finally a new string is returned.

Now, in the original post, I’m not clearly seeing how the lambda is being used.
lootBoxOne is a LootBox<Fedora> type. As such, it has access to all the LootBox functions, including fetch().
The second fetch() function takes an argument of type (T) -> (R). As lootBoxOne is a LootBox<Fedora>, the generic <T> parameter is a <Fedora> in this case, right?

So in the main function, we are defining a new val called coin, by calling the lootBoxOnes’s fetch function, which is expecting a lambda of type (T) -> (R).

And… I think I just got it. The lambda being passed into the function can be re-written as Coin(fedoraLoot -> fedoraLoot.value * 3).
Clearly, there is a type <T> (= <Fedora>) here: the fedoraLoot. I am mainly getting confused because the type <R> isn’t within a lambda format that I am accustomed to: on the other side of an arrow -> in a lambda. But indeed there is a new type being created here: a <Coin> type by calling the Coin class constructor.

So all in all, i guess there is a new type <R> = <Coin> being returned using information from a type <T> = <Fedora>.

Does that all make sense?

Yes. And the generic argument for <R> would be Coin.

Yes. To be more precise it expects a lambda of type (Fedora) -> R since the type parameter T is bound to lootBoxOne. Only the type parameter R is bound to the fetch function.

Not sure what you mean with this code. It’s not valid kotlin. I think what you mean is something like

val coin = lootBoxOne.fetch() { fedoraLoot -> Coin(fedoraLoot.value * 3) }

Going back to your original question:

If you have a lambda that takes exactly 1 parameter, you don’t have to name the parameter. Instead of writing

foo { myParameterName -> TODO(myParameterName) }

you can just use

foo { TODO(it) }

Kotlin will automatically name the parameter it. Other than that both expressions work exactly the same way.

So the next question would be: How does kotlin know which types to use for it and the return type.
The types for both are defined by your fetch function.
As you pointed out, it takes a lambda of type (T) -> R. We already know the type of T to be Fedora so that is used for it. The return type R can be anything so it will just become whatever the last expression in the lambda returns which is Coin. Now we know R = Coin so we also know that fetch will return a value of type Coin?, because fetch returns a value of type R?.

1 Like

A tangential thing I’d like to point out: anonymous functions and lambdas are different in Kotlin. Anonymous functions are defined with the fun keyword, just like named functions, and can have an explicit return type. Lambdas infer the return type automatically.

Also (from the docs):

A return statement without a label always returns from the function declared with the fun keyword. This means that a return inside a lambda expression will return from the enclosing function, whereas a return inside an anonymous function will return from the anonymous function itself.

1 Like