Max
Max

Reputation: 5081

Wrong "this" being used in nested closures

I'm trying to keep this minimal, but let me know if I'm being too minimal.

Suppose you have a class hierarchy like this, designed for generating HTML (inspired by the Kotlin tutorial; semi-pseudocode follows):

class Tag {
  protected val children = arrayListOf<Tag>()
  operator fun String.unaryPlus() = children.add(Text(this))
}
class TagWithChildren : Tag() {
  fun head(init: Head.() -> Unit) = initializeTag(Head(), init)
  fun script(init: Script.() -> Unit) = initializeTag(Script(), init)
  fun <T : Tag> initializeTag(tag: T, init: T.() -> Unit): T {
    tag.init()
    children.add(tag)
    return tag
  }
}
class Head : TagWithChildren()
class Script : Tag()
class Text(val str: Text) : Tag()

Notice that Head has head and script methods while Script doesn't.

Now you can construct a template that looks like this:

head {
    script {
        +"alert('hi');"
    }
}

Which works great! However, if the block passed to script tries to call methods that aren't available on Script, it can call the method on Head instead. For example,

head {
    script {
        script {
            +"alert('hi');"
        }
    }
}

not only isn't a compile error, it's actually equivalent to

head {
    script {
    }
    script {
        +"alert('hi');"
    }
}

which is super confusing, from a template author's perspective.

Is there any way to prevent method lookups from traveling up the scope like that? I only want it to look at the innermost scope.


UPDATE 11/24/2016: Kotlin 1.1-M03 has introduced scope control, which I believe solves exactly this problem. https://blog.jetbrains.com/kotlin/2016/11/kotlin-1-1-m03-is-here/

Upvotes: 6

Views: 688

Answers (2)

yole
yole

Reputation: 97148

The current behavior is intentional. Code in a lambda has access to receivers of all enclosing scopes. It is possible that a future version of Kotlin will add a modifier that will restrict a lambda with receiver to calling methods on that receiver only and not the enclosing scopes, but in the current version there's no way to change that behavior.

Upvotes: 3

Max
Max

Reputation: 5081

As a workaround, I can have it fail at runtime if I change the classes to look like this:

open class Tag {
  operator fun String.unaryPlus()
  // pulled up from TagWithChildren, call protected method
  fun head(init: Head.() -> Unit) = addChild(Head())
  fun script(init: Script.() -> Unit) = addChild(Head())

  // throws in Tag
  open protected fun addChild(t: Tag) = throw IllegalArgumentException()
}
class TagWithChildren : Tag() {
  // overridden to not throw in subclass
  protected override fun addChild(t: Tag) = children.add(t)
}

This way, every Tag has the builder methods (solving the scoping problem), but actually calling them may result in a runtime failure.

Upvotes: 2

Related Questions