ntos
ntos

Reputation: 319

Why is it necessary to create an instance of a class in this code?

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.

Upvotes: 0

Views: 86

Answers (1)

Sweeper
Sweeper

Reputation: 271440

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()
    [email protected](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.

Upvotes: 1

Related Questions