Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wrong "this" being used in nested closures

Tags:

kotlin

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/

like image 657
Max Avatar asked Jan 17 '16 03:01

Max


2 Answers

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.

like image 50
yole Avatar answered Nov 07 '22 15:11

yole


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.

like image 2
Max Avatar answered Nov 07 '22 15:11

Max