Friday, 2 September 2022

Kotlin: an afternoon learning Kotlin stuff

G'day:

This is going to be another fairly stream-of-consciousness effort, much like my earlier "Kotlin / Gradle / Kotest random exploration" article. I currently have very little direction to what I am going to do, but I thought I'd follow along here for the hell of it. As with the previous one (which worked out to be publishable, in the end), I don't even know if I will publish this article yet. I'll check what I end up with… at the end.


A (brief) return to Gradle Kotlin DSL Primer

Last time I meant to work through this lot, but got all of 6% (485 words out of 7268) of the way through before getting side tracked. I was hoping for better today, but I only got to 2% before stumbling across this:

Prerequisites

Yeah. OK. That makes. build.gradle.kts is written in Kotlin, so before knowing the DSL side of things, best I do some actual kotlin language stuff. These koans sound interesting, and following through the link I get to this on that Kotlin Koans page:

You can perform the tasks right inside IntelliJ IDEA or Android Studio by installing the EduTools plugin and choosing Kotlin Koans course.

Cool! So I'm doing that. I'll spare you the installation and getting it started and stuff.

This all looks quite cool and easy to use.

I'm gonna use each question as the basis to direct some of my own exploration, and write more tests (this is me, after all), in my scratch project.


Single-expression functions

Fist things first it's showing some unfamiliar function declaration syntax:

fun start(): String = TODO()

I'm guessing if a function has a single expression and it's the function result, we use this = syntax, so am just gonna run with that theory in a test:

package koans

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe

class FunctionSyntaxTest : DescribeSpec({
    describe("Checking syntax variations") {
        it("returns a single statement's value") {
            fun test():String = "EXPECTED_VALUE"

            test() shouldBe "EXPECTED_VALUE"
        }
    }
})

And this passes. And it fails if I initially expect an empty string, just to keep things red-green-etc.


joinToString and named arguments / defaulted parameters

Next they introduce the joinToString Collection method, specifically to show its named arguments and defaulted parameters (I'm gonna show you my tests, not the Koans stuff as their requirements are necessarily quite focused):

describe("Tests of joinToString") {

    val numbers = listOf<String>("tahi", "rua", "toru", "wha")

    it("takes separator (defaults to comma-space), prefix, postfix named parameters") {
        numbers.joinToString(postfix = ">", prefix = "<") shouldBe "<tahi, rua, toru, wha>"
    }

    it("takes a transformer callback which modifies each element") {
        numbers.joinToString("|", "<", ">") { "\"$it\""  } shouldBe """<"tahi"|"rua"|"toru"|"wha">"""
    }
}

Delimiting strings and escaping doube-quotes

I digress here also to look at this triple-doube-quote string delimiter ("raw strings") I spotted whilst working out if there was a nicer way to escape double quotes than \":

class StringTest : DescribeSpec({
     describe("delimiter tests") {
         it("can be delimited with three quotes (and double quote is otherwise escaped with a backslash") {
             val doubleQuote = """"""" // yeah there's 7 there
             doubleQuote shouldBe "\""
         }
     }
})

Yeah OK that is a daft way of expressing a string containing a double-quote, but it's funny. A bit. Well it is to me.


Variable scope & shadowing

Another side track. I'd set that numbers value in the parent scope, and figured that'd be fine. But also wanted to check I could override a val (immutable) variable in an inner scope:

describe("Checking how scoping of val works") {
    val testVal = "set in describe"

    it("can be accessed in a callback from an outer scope") {
        testVal shouldBe "set in describe"
    }

    it("can be overridden in a callback") {
        val testVal = "set in it"

        testVal shouldBe "set in it"
    }
}

All good. IntelliJ tells me that inner variable is shadowing another one:

The highlighting is there automatically; the tip appears if I hover on the hightlight.

I also note that when I run all my tests, I actually get a warning on this during the build phase:

w: C:\src\kotlin\scratch\src\test\kotlin\language\variables\VariablesTest.kt: (16, 17): Name shadowed: testVal

I will need to find out how to suppress that. I mean other than not shadowing the variable I mean.


Back to named arguments and defaulted parameters (briefly)

The koans notes observe what a fantastic feature of Kotlin this is that instead of all this Java boilerplate:

public String foo(String name, int number, boolean toUpperCase) {
    return (toUpperCase ? name.toUpperCase() : name) + number;
}
public String foo(String name, int number) {
    return foo(name, number, false);
}
public String foo(String name, boolean toUpperCase) {
    return foo(name, 42, toUpperCase);
}
public String foo(String name) {
    return foo(name, 42);
}

One can just do it with one method in Kotlin:

fun foo(name: String, number: Int = 42, toUpperCase: Boolean = false) =
        (if (toUpperCase) name.uppercase() else name) + number

This is quite cute coming from CFML when this has been a thing for 20 years now. And it's literally 20 years! CFMX 6.0 came out in 2002. 20 years ago. God I'm old.

I mean it's still a cool feature. Glad other languages are catching up ;-)


Raw strings and intelligent indentation-handling

This sums it up:

         it("can handle indentation in the triple-quoted string") {
             val message = """    
                 this is the message
             """.trimIndent()
        
             message shouldBe "this is the message"
         }

That is clever/handy.

And here's a second test wherein I have some trailing space on one of the lines of the string:

         it("respects trailing space on trimmed lines") {
             val message = """    
                 this has four space at the end    
             """.trimIndent()

             message shouldBe "this has four space at the end    "
         }

Good.

Here are another couple of examples where initially I thought it was behaving wrong, but I've since worked out why it does things the way it does:

         it("DOES NOT seem to trim indentation if the initial delimiter is not on a line by itself") {
             val message = """first line next to delimiter
                 second line indented
             """.trimIndent()

             // message shouldBe "first line next to delimiter\nsecond line indented"
             message shouldBe "first line next to delimiter\n                 second line indented"
         }

Initially I thought the commented-out expectation was what should happen, but the docs for trimIndent clearly state:

In case if there are non-blank lines with no leading whitespace characters (no indent at all) then the common indent is 0, and therefore this function doesn't change the indentation.

And here that first line - "first line next to delimiter" - has no indentation, sothe common indent is 0, therefore: no trim. All good.

And a second one that confused me:

         it("DOES NOT respect trailing space on the first delimiter line") {
             // the line below still has four trailing spaces
             val message = """    
                 this is indented
             """.trimIndent()

             //message shouldBe "    \ntthis is indented"
             message shouldBe "this is indented"
         }

As per the previous one: the commented-out expectation is what I was initially expecting here. However I'm being a div. I had whittled that case down from the one where there was trailing spaces at the end of the line. But once I remove all the rest of the content from the line it's not trailing space, it's at the beginning of the line. IE: indentation. Which I'm asking to strip.

I really should not expect Kotlin to be so easy to find issues with as ColdFusion or Lucee. I must remember that.

Back in the "that's cool" category: as soon as I typed a multi-line raw string, IntelliJ added the .trimIndent() call. After typing the three double-quotes:

After pressing enter:


So that was a bit random. I kinda cut off quickly there cos that represents my afternoon's investigations. I'm not sure how useful this is to anyone else, but I'm gonna press "publish" anyhow. Why not. It's my blog :-p

Righto.

--
Adam