0

When I comment out one line with // in this code, it doesn't work as expected.


open class Tag(val name: String) {
    private val children = mutableListOf<Tag>()

    protected fun <T : Tag> doInit(child: T, init: T.() -> Unit) {
        println("$child  passed to doInit.")
        init(child)
        children.add(child)
        println("$child  added")
    }

    override fun toString(): String {
        println("toString called and ..now " +
                "we have: <$name>${children.toString()}</$name>\"")
        return "<$name>${children.toString()}</$name>"
    }
}

fun table(init: TABLE.() -> Unit): TABLE {
    println("table called")
    return TABLE().apply(init)
}

class TABLE : Tag("table") {
    fun tr(init: TR.() -> Unit) {
        println("tr called")
        doInit(TR(), init);
        println("after tr's doInit called")
    }
}
class TR : Tag("tr") {
    fun td(init: TD.() -> Unit) {
        println("td called")
        doInit(TD(), init);
        println("after td's doInit called")
    }
}
class TD : Tag("td")

fun createTable() =
        table {
            tr {
                td {
                }
            }
        }

  1. Even when I comment out init(child), fun createTable1() = table{tr{}} works as expected. It calls doInit, and produces:

    <table><tr></tr></table>
    
  2. But fun createTable2() = table{tr{td{}}} doesn't call doInit on td. It produces:

    <table><tr></tr></table> 
    

    and not:

    <table><tr><td></td></tr></table>
    

Thank you very much for reading.

ntos
  • 189
  • 10
  • Well yeah, commenting out `init(child)` will break `createTable2`. Why is this surprising to you? – Sweeper Aug 16 '21 at 01:24
  • In other words, what is your expected result for `createTable1` when `init(child)` is commented out? – Sweeper Aug 16 '21 at 01:55
  • 1. We pass an instance of TR or TD to doInit(). Why do we need to create it one more time inside doInt()? and 2. Why is doInit() called in createTable1() and not called in createTable2() – ntos Aug 16 '21 at 04:04
  • Okay, I can see why you are confused with the first point. Regarding the second point, `doInit` _is_ called in `createTable2` as well! Do you not see `before doinit.tr called` and `doinit called` being printed? The same thing is printed in `createTable1`. – Sweeper Aug 16 '21 at 04:11
  • No. When I comment out init(child), doInit() in td is not called at all. That's why the td Tag is never inserted to children. Here's the result on my pc running android Studio 4. `table called; before doinit.tr called; doinit called; tr called;
    `. As you can see, doInit is called one once.
    – ntos Aug 16 '21 at 04:50
  • I'm sorry. I mean `createTable2()` doesn't call doInit in `td()` – ntos Aug 16 '21 at 04:59

1 Answers1

1

We pass an instance of TR or TD to doInit(). Why do we need to create it one more time inside doInit()?

No, init(child) does not create a new instance. It just calls init, which is the second parameter of doInit. Don't get put off by the word init. It could be named f or g and you would still get the same result. It's just a function.

Here I've renamed some of the things. See if this helps:

open class Tag(val name: String) {
    private val children = mutableListOf<Tag>()

    protected fun <T : Tag> applyAndAddAsChild(child: T, lambda: T.() -> Unit) {
        lambda(child)
        children.add(child)
        println("doinit called")
    }

    override fun toString() =
        "<$name>${children.joinToString("")}</$name>"
}

fun table(lambda: TABLE.() -> Unit): TABLE { println("table called"); return TABLE().apply(lambda)}

class TABLE : Tag("table") {
    fun tr(lambda: TR.() -> Unit) { println("before doinit.tr called"); applyAndAddAsChild(TR(), lambda); println("tr called")}
}
class TR : Tag("tr") {
    fun td(lambda: TD.() -> Unit) { println("before doinit.td called"); applyAndAddAsChild(TD(), lambda); println("td called")}
}

Anyway, you call init by passing child, the first parameter of doInit, as an argument. As a side note, notice that the type of init is T.() -> Unit. This means that init can also be called like this: child.init(), which is arguably more natural.

What does init do? Well, since it is a parameter, let's see what the callers of doInit has passed to it!

// println calls removed for brevity 
fun tr(init: TR.() -> Unit) { doInit(TR(), init) }
fun td(init: TD.() -> Unit) { doInit(TD(), init) }

So init is actually the lambda arguments after tr and td!

In the case of

table { tr { td { } } }

You pass the lambda argument { td { } } to tr, so init is td { }. Now tr executes, which calls doInit, and if init(child) is commented, init won't be called, so td won't be called, which means that doInit for td won't be called.

Commenting out init(child) makes no difference in the case of

table { tr { } }

because the lambda argument for tr is { }, aka "do nothing". So no matter you comment out init(child) or not, you do nothing.

It feels kind of weird to have a doInit that takes a thing and another function, just to call the function with the thing as parameter. IMO, the code would look nicer if doInit were declared like this:

protected fun <T : Tag> T.applyAndAddAsChild(init: T.() -> Unit) {
    init()
    this@Tag.children.add(this)
}

Then the tr and td functions would have the same "shape" as table:

// in "table" you can just apply the lambda, but in tr and td you have to 
// add the new tag as a child too, which is the extra thing that 
// applyAndAddAsChild does
class TABLE : Tag("table") {
    fun tr(init: TR.() -> Unit) = TR().applyAndAddAsChild(init)
}
class TR : Tag("tr") {
    fun td(init: TD.() -> Unit) = TD().applyAndAddAsChild(init)
}
fun table(init: TABLE.() -> Unit) = TABLE().apply(init)

Hopefully you see that there is a nice symmetry going on here.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • 1
    I still don't understand your explanation from "you pass the lambda ... td won't be called". Please tell me if my understanding is ok with this code: `fun tr(init: TR.() -> Unit) { doInit(TR(), init) }`. When you pass in a lambda, you are not required to pass in a parameter. I can't in this case, the parameter is the implicit 'this: TR'. What do I want to do with 'this'? Nothing. I want to fiddle with the body of the lambda so I can pass in a string or 1+2. So I don't understand why `..so init is td { }`. `init` is "TR.() -> Unit". `td` is just like a string and should call its own doInit. – ntos Aug 16 '21 at 06:49
  • @ntos You don't seem to understand what a lambda is? The lambda _is_ the parameter! "What do I want to do with `this`? Nothing." Well, actually `table { tr { td { } } }` is the same as `table { this.tr { this.td { } } }`, so you _are_ doing things with `this`. You might want to read [this](https://kotlinlang.org/docs/lambdas.html) and [this](https://stackoverflow.com/questions/45875491/what-is-a-receiver-in-kotlin). Note that `TR.() -> Unit` is a function with a _receiver_ of `TR`, in case you don't know that already. – Sweeper Aug 16 '21 at 06:50
  • @ntos `init` is `TR.() -> Unit`, yes that is its type. By "`init` is `td { }`", what I meant was that `init` is a function that will call `td { }`, so if you don't call `init`, `td { }` won't be called. And no `td` is not a string. `td`'s `doInit` will only be called if you call `td`. – Sweeper Aug 16 '21 at 06:53
  • Well, I am new to kotlin you know. And yes this short comment of yours does wonder. I understand it now. Thank you so much. How can I give it a tick? But don't remove your answer. I may have to come back you know. – ntos Aug 16 '21 at 06:53
  • You say that `init is a function that will call td { }, so if you don't call init, td { } won't be called`. Then why is `createTable(table{tr})` able to call `tr` when `init(child)` is commented? – ntos Aug 16 '21 at 07:04
  • @ntos Good question! If you haven't noticed already, `table` is the odd one out here. It doesn't call `doInit` (it doesn't have parent to add itself to, after all), so it doesn't care about whether or not a line in `doInit` is commented out or not. Rather, it calls `apply` directly, applying the lambda that calls `tr`. – Sweeper Aug 16 '21 at 07:06