Saturday, 10 September 2022

Kotlin: looking at Numbers, more Kotest stuff, and something about "function literals with receivers"?

G'day:

Here's some new unguided Kotlin experimentation, following on from other similar articles.

Firstly I inadvertantly finished yesterday's article when I was sitting on the last exercise in the first set of Kotlin Koans exercises, and finishing that off was about a dozen obvious keystrokes (it % 2 == 0) so I probably should have finished that article triumphantly with that.

Instead I look at it this morning and went "oh that was super easy, barely an inconvenience", and finished it off. So much for story pacing.


Numbers in Kotlin

The code for these examples is on GitHub: NumberTest.kt.

I did need to do a bit of research to nail those 11 keystrokes for the last exercise. It was to finish a lambda that was the predicate for an any call to determine there there were any even numbers in a set. I initially wondered - given Kotlin seems to have a method for everything else - whether it might have an isEven method, so looked into that. No, it doesn't. Just use modulo division. But anyway, I landed on the docs page for Numbers, and had a look at that.

As always, my investigation was implemented via tests:

it("demonstrates ints and longs of same 'human value' are not equal") {
    val i :Int = 24
    val l :Long = 24L

    shouldThrow<AssertionError> {
        i.equals(l).shouldBeTrue()
    }
}

Note I don't need the explicit types in the variable declaration there, I felt it made it more clear what I was doing. Note the L suffix on the Long literal.


it("demonstrates Kotest will handle the casting between ints and longs internally") {
    val i :Int = 17
    val l :Long = 17L

    i shouldBe l
}

it("demonstrates Kotest will handle the casting between ints and floats internally") {
    val i :Int = 17
    val f :Float = 17.0F

    i shouldBe f
}

it("demonstrates Kotest will handle the casting between ints and doubles internally") {
    val i :Int = 17
    val d :Double = 17.0

    i shouldBe d
}

Am quite pleased Kotest takes care of the type check so disparate types can be checked in a human sense. Note the difference in literals for floats (F suffix) and Doubles (just a decimal number).


it("demonstrates one can convert an int to a double") {
    val i :Int = 17
    val d :Double = 17.0

    i.toDouble().equals(d).shouldBeTrue()
}

There are conversion methods to/from all numeric types.


it("demonstrates literals can have _ as a separator") {
    val i :Int = 123_456_789

    i shouldBe 123456789
}

it("demonstrates _ is not just a 1000s separator") {
    val d :Double = 3_6_5.2_4_2_5

    d shouldBe 365.2425
}

One thoughtful thing is that for clarity one can put an underscore in any numeric literal to make groupings clear. Not that my latter example is more clear than just 365.2425, but it's demonstrating the point.


it("has binary and hex literals") {
    val b = 0b1111_1111_1111_1111
    var h = 0xff_ff

    b shouldBe h
}

it("demonstrates binary and hex numbers of same 'human value' ARE equal") {
    val b = 0b1111_1111_1111_1111
    val h = 0xff_ff

    b.equals(h).shouldBeTrue()
    b.shouldBeTypeOf<Int>()
    h.shouldBeTypeOf<Int>()
}

Kotlin also has binary and hex literals (but not octal ones. No great loss in my books). Those literals are just ways of defining an Int, they are not their own numeric sub-type.

There's undoubtedly more to look at with numbers, but my attention got shifted when working out how that shouldBeTypeOf<Int> matcher works.


Type-checking in Kotest matchers

The noteworthy ones are:

  • shouldBeTypeOf<type> matches the exact type (subclasses will not match).
  • shouldBeInstanceOf<type> matches within the type-hierarchy (subclasses will match).

Examples:

describe("Tests of type-checking") {
    it("checks exact type with shouldBeTypeOf") {
        val i = 24

        i.shouldBeTypeOf<Int>()
    }

    it("checks subtype with shouldBeInstanceOf") {
        val i = 17

        shouldThrow<AssertionError> {
            i.shouldBeTypeOf<Number>()
        }
        i.shouldBeInstanceOf<Number>()
    }
}

The code for this example is on GitHub: MatcherTest.kt.


Kotest clues

One neat trick one can do in Kotest is to provide a clue for an expectation:

it("can fail with a clue") {
    try {
        withClue("this is the clue") {
            fail("this is a failure message")
        }
    } catch (e :AssertionError) {
        e.message shouldBe """
            this is the clue
            this is a failure message
        """.trimIndent()
    }
}

This is useful both in the code to make it more clear why we're bothering to assert what we are, and it also comes out in the test failure feedback too. This is what I get if I take out the try/catch from that test above, and run it:

C:\Users\camer\.jdks\openjdk-17.0.1\bin\java.exe
Testing started at 16:28 ...

Misc Kotest tests -- emits a failure message is excluded by test filter(s): Excluded by test path filter: 'Misc Kotest tests -- can fail with a clue'

this is the clue
java.lang.AssertionError: this is the clue
this is a failure message
	at system.kotest.KotestTest$1$1$2.invokeSuspend(KotestTest.kt:25)

I realise Jasmine-esque test framework support this, eg:

expect(actual).toBe(expected, "careful now")

So does xUnit-ish frameworks:

asertEquals(expected, actual, "down with this sort of thing")

But I think Kotest's approach is clearer / tidier / less-of-an-afterthought.


One can also use the asClue method to pass an object into a block of expectations, and that object is then reflected as the clue for the failure:

it("can attach asClue to an object to use it for multiple expectations") {
    val colours = listOf("whero","karaka","kōwhai","kākāriki","kikorangi","poropango","papura")

    try {
        colours.asClue {
            withClue("should not contain green") {
                it.none{it == "kākāriki"}.shouldBeTrue()
            }
        }
    } catch (e :AssertionError) {
        e.message shouldBe """
            [whero, karaka, kōwhai, kākāriki, kikorangi, poropango, papura]
            should not contain green
            expected:<true> but was:<false>
        """.trimIndent()
    }
}

Here I have a clue on the object, and also on the specific expecation.

Note the outer it is a reference to the object being used as the clue. The inner it is a reference to current element of iterating over… the outer it… for the none (none iterates over a collection and returns false as soon as the predicate returns true of any element). The it usage is slightly confusing there, but that's down to me being lazy. And so I could write this paragraph. I could have written that block with some clearer parameter names:

colours.asClue {
    colours ->
    withClue("should not contain green") {
        colours.none{colour -> colour == "kākāriki"}.shouldBeTrue()
    }
}

TBH, I don't know that that is any better.

The code for these examples is on GitHub: KotestTest.kt.


Kotest soft assertions

This solves a problem that has caused me grief a lot of times with other testing frameworks. A test demonstrates how this works:

it("can use soft assertions to let multiple assertions fail and report back on all of them") {
    val actual :Int = 15

    try {
        assertSoftly {
            actual.shouldBeTypeOf<Number>()
            withClue("should be 15.0") {
                actual.equals(15.0).shouldBeTrue()
            }
            actual shouldBe 15
            actual shouldBe 16
        }
    } catch (e :MultiAssertionError) {
        assertSoftly(e.message) {
            shouldContain("1) 15 should be of type kotlin.Number")
            shouldContain("2) should be 15.0")
            shouldContain("expected:<true> but was:<false>")
            shouldContain("3) expected:<16> but was:<15>")
        }
    }
}

Here all of those expectations fail. On other testing frameworks I have used they always bail-out as soon as the first expectation fails, and the rest aren't tested. Often this is fine and exactly correct because subsequent expectations might rely on earlier ones passing. But this is not always they case. Obvs one could create one's own custom expectation to check a whole wodge of stuff at once, but it's nicer to have a simple block construct that JFDI.

Another thing to note is the two forms of assertSoftly. The first rendition just takes a lambda with no argument, and the body of the lambda is implemented the same as one usually would in a test. The second variant takes and argument, and that argument is used as the input for all the expectations. I actually dunno how this syntax works, and I have some reading to do. For now: handy to know, and a good reduction of code repetition. And is also encourages the expectations to stay focused, given they must all be applied to the same object.

Be mindful that my test is contived there: it's testing the behaviour of the failure, rather than being a failing test. Normally the test would be more like this:

it("can use soft assertions to let multiple assertions fail and report back on all of them") {
    val actual :Int = 15

    assertSoftly {
        actual.shouldBeTypeOf<Number>()
        withClue("should be 15.0") {
            actual.equals(15.0).shouldBeTrue()
        }
        actual shouldBe 15
        actual shouldBe 16
    }
}

And given that's a failing test now, we get that in the test-run feedback:

io.kotest.assertions.MultiAssertionError: 
The following 3 assertions failed:
1) 15 should be of type kotlin.Number
	at system.kotest.KotestTest$1$2$1.invokeSuspend(KotestTest.kt:78)
2) should be 15.0
expected:<true> but was:<false>
	at system.kotest.KotestTest$1$2$1.invokeSuspend(KotestTest.kt:63)
3) expected:<16> but was:<15>
	at system.kotest.KotestTest$1$2$1.invokeSuspend(KotestTest.kt:66)

Good stuff.

The code for these examples is on GitHub: KotestTest.kt.


Kotest shouldThrowMessage

This is just a quick one. I didn't think it fitted in any of the other sections. I'm catching a lot of exceptions to test how things "fail" in tests, but still have a passing test. Generally I've been expectimng a specofoc exception type to be thrown, but one can also just look for a specific message:

it("should throw an exception with a specific message") {
    class InquisitionException(message: String?) : Exception(message)

    shouldThrowMessage("No-one expects the... InquisitionException") {
        throw InquisitionException("No-one expects the... InquisitionException")
    }
}

Handy.

The code for this example is on GitHub: MatcherTest.kt.


Function literals with receivers

Further up I was bemused by how this code works:

assertSoftly(e.message) {
    shouldContain("1) 15 should be of type kotlin.Number")
    shouldContain("2) should be 15.0")
    shouldContain("expected:<true> but was:<false>")
    shouldContain("3) expected:<16> but was:<15>")
}

Somehow shouldContain is implicitly being called on the e.message argument I am passing to assertSoftly. IE: to my ignorant eyes I am expecting to be seeing something like this:

assertSoftly(e.message) {
    someThingHere shouldContain("1) 15 should be of type kotlin.Number")
    someThingHere shouldContain("2) should be 15.0")
    someThingHere shouldContain("expected:<true> but was:<false>")
    someThingHere shouldContain("3) expected:<16> but was:<15>")
}

Turns out it's not just magic, it's a feature of function literals in Kotlin. Now I only barely grasp what these docs are saying, but I've made enough sense of them to write some code to test it:

Function types with receiver, such as A.(B) -> C, can be instantiated with a special form of function literals – function literals with receiver.

As mentioned above, Kotlin provides the ability to call an instance of a function type with receiver while providing the receiver object.

Inside the body of the function literal, the receiver object passed to a call becomes an implicit this, so that you can access the members of that receiver object without any additional qualifiers, or access the receiver object using a this expression.

Just to start with, here's a no-surprises function literal:

it("is a baseline test using a simple function expression") {
    val add = {op1:Int, op2:Int -> op1 + op2}

    add(17, 24) shouldBe 41
}

It takes two operands, and adds them.

Stealing the example from the docs, we have this alternative:

it("uses function literals with receivers") {
    val add: Int.(Int) -> Int = { other -> this.plus(other) }

    add(17, 24) shouldBe 41
    17.add(24) shouldBe 41
}

This is nice syntax from Kotlin. Not only can one call this function the "normal" way, one can also call that function with the first argument as a receiver (I guess that's what they mean as a receiver?), and it looks like a method on Int class (the literal 17 is still an Int object).

The implementation above doesn't use idiomatic-Kotlin though, in how I have explicitly referenced this (a reference to the first argument). Here's what they mean about "so that you can access the members of that receiver object without […] using a this expression":

it("uses function literals with receivers and implicit this") {
    val add: Int.(Int) -> Int = { other -> plus(other) } // see there's no this here any more

    add(17, 24) shouldBe 41
    17.add(24) shouldBe 41
}

That's all that assertSoftly function expression is doing.

Just so I was clear on this, I created another test:

it("checks the values of the operands in the function implementation") {
    var op1 = 0
    var op2 = 0
    val add: Int.(Int) -> Int = {
        other ->
        op1 = this
        op2 = other
        this.plus(other)
    }

    17.add(24) shouldBe 41

    op1 shouldBe 17
    op2 shouldBe 24
}

This passes, so my hypothesis is right.

My grasp of how the docs explain this is still tenuous. I didn't really see how it worked until I tried it, so I need to think some more about this stuff. Or perhaps better understanding will come from just doing. I have to say that this synxtax doesn't mean much to me: Int.(Int) -> Int = {}, and I can't correlate it back to add(17, 24) and 17.add(24).

I guess looking at the assigment from the test:

val add: Int.(Int) -> Int = { other -> plus(other) }

That's just this:

val variableName: type = value

Where:

  • add is the variableName
  • Int.(Int) -> Int is the type
  • { other -> plus(other) } is the value

The only confusing bit is the type, but I guess if I use different types it might make more sense:

it("clarifies the syntax a bit by using different types") {
    val repeat: String.(Int) -> String = { other -> repeat(other) }

    "Z".repeat(11) shouldBe "ZZZZZZZZZZZ"
}

This is a bit clearer. The type String.(Int) -> String is, I guess, a function that takes a String and an Int, and returns a String. And the String.(Int) syntax means that it can be called using the arg1.function(arg2) syntax.

I think.

The code for these examples is on GitHub: FunctionSyntaxTest.kt.


Anyway, I've been at this a while now: the amount of writing I do (blog and code) only represents about 1/3 of the time I spend reading stuff. So I've been at this most of the day now. Time for a beer. And maybe dinner.

Righto.

--
Adam


PS: I made zero new progress on the koans today, but I learned a lot I reckon.