Sunday 4 September 2022

Kotlin: there's no such thing as static, apparently

G'day:

Whilst writing today's previous article ("CFML: invokeImplicitAccessor on CFCs"), I noted that CFML does not support implicit accessor methods on static properties.

I wanted to look at how another language handles the same, and the only languages I personally know anything about that has accessors defined on the properties themselves are C# and Kotlin (that's not to say there aren't others, undoubtedly it's very prevalent; those're the two I know about). My C# is even worse than my Kotlin (if you can believe that), so I decided to check Kotlin. Plus, like, I need to know about Kotlin anyhow, so makes sense.

I was quite surprised to see that Kotlin simply doesn't support the concept of static properties. I mean like I initially went "huh? What? Why?", but figured there was a good reason. I found how Kotlin approximates the equivalent fairly quickly, but it too me longer to find out why.

During all this I tested how property accessors work in Kotlin anyhow, and here's some code for standard property accessors:

describe("property accessor tests") {
    class Person {
        var firstName = ""
            set(value) {
                field = value.replaceFirstChar(Char::titlecase)
            }
        var lastName = ""
            set(value) {
                field = value.replaceFirstChar(Char::titlecase)
            }

        val fullName
            get() = "$firstName $lastName"
    }

    it("uses getters/setters") {
        var person = Person()
        person.firstName = "jane"
        person.lastName = "roe"

        person.fullName shouldBe "Jane Roe"

    }
}

That's pretty self-explanatory, and no surprises there I think. I guess I was thrown by needing to use field rather than the name of the property. It's "explained" in Properties › Getters and setters › Backing fields, but only as a statement of fact; not why it is the way it is. I did note though, that if I had this:

var firstName = ""
    set(value) {
        firstName = value.replaceFirstChar(Char::titlecase)
    }

Then I get a stack overflow. I am guessing that assignment to firstName itself calls its setter, so we get a bit of a recursive meltdown there. I should test that, I think.

OK, so that's normal properties. What about approximating static ones? Kotlin has this concept of a "Companion objects" which one can define within a class, and - as the docs say - "Members of the companion object can be called simply by using the class name as the qualifier". I'm not gonna re-quote someone else's docs too comprehensively, so just go read them if you like.

The "static" equivalent tests here would be this lot:

class PropertiesTest : DescribeSpec ({

    // ...

    describe("static property accessor tests") {
        it("uses getters/setters") {
            StaticPerson.firstName = "connor"
            StaticPerson.lastName = "macLeod"

            StaticPerson.fullName shouldBe "Connor MacLeod"
        }
    }
})

class StaticPerson {
    companion object OnlyOne {
        var firstName = ""
            set(value) {
                field = value.replaceFirstChar(Char::titlecase)
            }
        var lastName = ""
            set(value) {
                field = value.replaceFirstChar(Char::titlecase)
            }

        val fullName
            get() = "$firstName $lastName"
    }
}

Another observation to make here is that I have not declared StaticPerson as a nested class here, like I did with the "property accessor tests", above. I tried that, but got an error: Modifier 'companion' is not applicable inside 'local class'. I could not find out why this is the case, but: so be it.

I found a good explanation of why Kotlin doesn't implement the concept of static: Why is there no static keyword in Kotlin?. It's worth reading a bunch of the answers. One of the answers refers back to the docs (Object expressions and declarations › Object declarations › Companion objects):

Note that even though the members of companion objects look like static members in other languages, at runtime those are still instance members of real objects, and can, for example, implement interfaces[…]

From that and the other reading, Kotlin does it this way as it makes things more OO than Java's approach. Seems legit, I guess.


I just tested that theory I had about property assignments within the class itself also call the property's setter, and this proves to be correct:

describe("property accessor tests") {
    class Person {
        var firstName = ""
            set(value) {
                field = value.replaceFirstChar(Char::titlecase)
            }
        var lastName = ""
            set(value) {
                field = value.replaceFirstChar(Char::titlecase)
            }

        val fullName
            get() = "$firstName $lastName"

        fun setBoth(firstName:String, lastName:String) {
            this.firstName = firstName
            this.lastName = lastName
        }
    }

    // ...
    
    it("uses setters on internal property assignments") {
        var person = Person()
        person.setBoth("sojourner", "truth")

        person.fullName shouldBe "Sojourner Truth"
    }
}

If it wasn't calling the setters on firstName and lastName, then fullName would not be capitalised.

That's enough learning for a Sunday.

Righto.

--
Adam