Saturday 27 May 2023

Usage of "this"

Since quite some time I find the existence of the this keyword in most programming languages not particularly helpful, and I prefer the Python approach of declaring the "receiver" in the function signature and using your name of choice (normally "self"). These are some eeasons why I'm not much keen of the "this" keyword:

Implicit use. In languages like C#, Kotlin, C++ (I think) you can skip typing "this". It will be inferred by the compiler where it makes sense. I find this confusing, at I prefer always refering to it explicitly. JavaScript forces you to write "this" if you want to use it.

Different meanings. In JavaScript "this" can refer to different things. "Normal" functions have dynamic this and Arrow functions have lexical this. When we look up a function in an object and invoke it, if that's a normal function it will dynamically receive as "this" that object on which it has been looked up (what in Kotlin we call "the receiver"), but if it's an arrow function, it won't get that object and will search for it in its lexical scope, so it will find the "this" that was there when the arrow function was defined. When we invoke a "Normal" function not through an object look up it receives as "this" a sort of global object (that's the window object when running in the browser, or another sort of "global" object if running for example in Node). Additionally, "Normal" functions can get bound a static "this" by means of invoking the bind function.

Closures. In JavaScript "Normal functions" (I mean, non Arrow Functions), as I've just explained, "this" is either the object on which the function is invoked (receiver) or a global one, so if we want to trap "this" in a closure we have to use the trick of referencing it with another variable (the normal convention is doing let self = this;) and use that new variable in the closure. On the other hand, in C# closures trap the "this" of the scope where the function is declared (the enclosing scope).

Kotlin adds some extra complexity to the "this" world, with the labelled qualifiers thing:

If this has no qualifiers, it refers to the innermost enclosing scope. To refer to this in other scopes, label qualifiers are used

So in functions with receiver (a member of a class, an extension function, a function literal with receiver) "this" is the object on which we are invoking the function, but additionally if we have nested scopes (inner clasess, several levels of nested functions...) we can get access to the "this" of an outer scope using a "qualified this".

When there is no receiver (anonymous functions or lambdas without receiver) these function can trap the "this" of the enclosing scope in its closure (so it's the same behaviour that we have for C#). Using a rather artificial example:



class Person constructor(var name: String, var age: Int) {
    fun createBookManager1(): (String) -> String {
        // anonymous function WITHOUT receiver, so the "this" is the one of the innermost enclosing scope 
        val manager: (String) -> String = fun(title: String): String {
            return "${this.name} has checked book: ${title}"
        }
        return manager;
    }

    fun createBookManager2(): (String) -> String {
        // lambda WITHOUT receiver, so the "this" is the one of the innermost enclosing scope 
        val manager: (String) -> String = {
            "${this.name} has checked book: ${it}"
        }
        return manager;
    }

    fun createBookManager3(): Person.(String) -> String {
        // function type WITH receiver 
        val manager: Person.(String) -> String = {
            //"this" is not trapped by the closure, it's the receiver that it receives when invoking the function
            "${this.name} has checked book: ${it}"
        }
        return manager;
    }

    fun createBookManager4(): Person.(String) -> String {
        // function type WITH receiver, to refer to the "class this" we use a qualified this 
        val manager: Person.(String) -> String = {
            //"this" is not trapped by the closure, it's the receiver that it receives when invoking the function
            "${this.name} has checked book: ${it} and has trapped ${this@Person.name}"
        }
        return manager;
    }

}


fun main() {
    val person = Person("Xuan", 22)
    val manager1 = person.createBookManager1()
    println(manager1("Book1"))

    val manager2 = person.createBookManager2()
    println(manager2("Book1"))

    val manager3 = person.createBookManager3()
    println(person.manager3("Book1"))

    val manager4 = person.createBookManager4()
    val person2 = Person("Francois", 5)
    println(person2.manager4("Book1"))

    // Xuan has checked book: Book1
    // Xuan has checked book: Book1
    // Xuan has checked book: Book1
    // Francois has checked book: Book1 and has trapped Xuan


}

No comments:

Post a Comment