G'day:
Because no-one has specifically screams "FFS stop it, Cameron", I'm gonna continue with another random Kotlin noobie investigation / brain dump. Previous similar articles here.
The Nothing and Unit types
Nothing
This came up in the next koans exercise. I read the docs, and looked at an answer on Stack Overflow ("Kotlin - Void vs. Unit vs. Nothing). The key bit seems to be the word cannot in this sentence: "If a function has return type Nothing, then it cannot return normally. It either has to throw an exception, or enter an infinite loop". And the next sentence is an interesting one: "Code that follows a call to a function with return type Nothing will be marked as unreachable by the Kotlin compiler". I wrote a test (obviously ;-)):
class NothingTest : DescribeSpec( {
describe("Tests of the Nothing type") {
val EOL = System.lineSeparator()
val output = SystemLambda.tapSystemOut {
try {
outputUntilDie(3)
} catch (e: Exception) {
println(e.message)
}
}
output.trim() shouldBe "Iteration 1${EOL}Iteration 2${EOL}Iteration 3${EOL}It died"
}
})
fun outputUntilDie(max :Int) :Nothing {
var current = 0
while (true) {
current++
println("Iteration $current")
if (current >= max) {
throw Exception("It died")
}
}
}
The code below is on Github @ NothingTest.kt
Note in outputUntilDie there is never a normal return condition. The loop either goes forever (assuming I could somehow pass it a max of infinity), or the code bombs out. So whatever comes after the call to outputUntilDie can never be run. If I try to add some, IntelliJ lets me know:
And the compiler gives a warning:
> Task :compileTestKotlin w: C:\src\kotlin\scratch\src\test\kotlin\language\types\NothingTest.kt: (15, 17): Unreachable code
If I was to change the return type to be Unit (returns no value, as opposed to returns nothing), then I don't get the visual cue or warning.
So I guess it's handy in that regard.
OK, the koans exercise demonstrates a more realistic situation where the code being considered unreachable is important.
import java.lang.IllegalArgumentException
fun failWithWrongAge(age: Int?) {
throw IllegalArgumentException("Wrong age: $age")
}
fun checkAge(age: Int?) {
if (age == null || age !in 0..150) failWithWrongAge(age)
println("Congrats! Next year you'll be ${age + 1}.")
}
fun main() {
checkAge(10)
}
Here the code won't even compile, because it's possible to get to ${age + 1} with age being null:
C:\src\kotlin\Kotlin Koans\Introduction\Nothing type\src\Task.kt:9:50 Kotlin: Operator call corresponds to a dot-qualified call 'age.plus(1)' which is not allowed on a nullable receiver 'age'.
Telling it that failWithWrongAge has a return type of Nothing mitigates this as processing could have halted already by then.
Unit
And the Unit type is a type that is equivalent to void, it's just a more OO way of doing things, cos it is actually a type, and there is a Unit object. This means everything returns an object, and there's no "special" handling like in Java with void. It's also the default return type in Kotlin according to the docs, so one doesn't need to specify it.
Lambdas
Here I've just messed around with some common higher-order functions available on collections. The code below is on Github @ FunctionsTest.kt
it("is a basic PoC test") {
val strings = listOf("whero", "kākāriki", "kikorangi")
val lengths = strings.map {it.length}
lengths.joinToString(",") shouldBe "5,8,9"
}
map just takes a single param that is the mapping function, so no need for parentheses. And because the callback only receives the one argument, one can just use the default it name. So {it.length} is a lambda that returns the length property value of its it argument (ie: String.length).
class Number(var value: Int, var en: String, var mi: String) {}
it("chains some together") {
val jumbledNumbers = listOf(
Number(2, "two", "rua"),
Number(4, "four", "wha"),
Number(3, "three", "toru"),
Number(1, "one", "tahi")
)
val maoriNumbers = jumbledNumbers.sortedBy{it.value}.map{it.mi}
maoriNumbers.joinToString(",") shouldBe "tahi,rua,toru,wha"
}
More of the same except I'm chaining some methods together here.
it("shows using _ to ignore a param in a lambda") {
val numbers = mapOf(
Pair("one", "tahi"),
Pair("two", "rua"),
Pair("three", "toru"),
Pair("four", "wha")
)
val output = SystemLambda.tapSystemOut {
numbers.forEach{(_, maori) -> print("$maori")}
}
output.trim() shouldBe "tahiruatoruwha"
}
The docs made a point of mentioning this, so I might as well. It's right in the Kotlin idiom to use _ as a parameter name when your lambda receives some arguments it doesn't need. Here the key is the first parameter (so the English version of the number), and I don't need that so this is made explicit by just using _. It's only the second argument I want: the Māori version
class Number(var value: Int, var en: String, var mi: String) {}
it("uses an anonymous function") {
val jumbledNumbers = listOf(
Number(2, "two", "rua"),
Number(4, "four", "wha"),
Number(3, "three", "toru"),
Number(1, "one", "tahi")
)
val maoriNumbers = jumbledNumbers.sortedWith(fun(e1, e2) = e1.value - e2.value).map{it.mi}
maoriNumbers.joinToString(",") shouldBe "tahi,rua,toru,wha"
}
For the hell of it here I use the anonymous function syntax (so with the fun keyword) for sortedWith comparator lambda. As the comparator method is just one expression, I don't need the parentheses or the return statement.
it("is an example of a fold") {
val rgb = listOf("whero", "kākāriki", "kikorangi")
val colours = rgb.fold("") {
colours, colour -> if (colours.isEmpty()) colour else "$colours,$colour"
}
colours shouldBe "whero,kākāriki,kikorangi"
}
And with the block syntax I had no idea how to define my own params (instead of just the situation where there's only one, and the default it would do), but it's pretty straight forward as it turns out. The bit I didn't guess right was the parameter definitions are inside the block, not before it like I kinda wanted to do.
I also found out that there is no ternary operator (? :) in Kotlin. if is an expression, so it does the same thing. Slightly more verbose, but it's OK. I don't like using else in my code though (but I don't might the "else" operand when using ? :. Hrm…?
it("is an example of a reduce") {
val redWhiteAndBlue = listOf("whero", "mā", "kikorangi")
val colours = redWhiteAndBlue.reduce {colours, colour -> "$colours,$colour"}
colours shouldBe "whero,mā,kikorangi"
}
This shows ths difference between fold and reduce:
- fold takes a specific initial value as a first argument.
- reduce uses the first element of the collection as the initial value.
groupBy higher-order function
Ooh I just spotted this in the docs. Kotlin has a gropupBy higher-order function. It does what it says on the tin. here's a contrived example:
it("groups the words by their first character, capitalising each word in the result") {
val words = listOf(
"tahi","rua","toru","wha","rima","ono","whitu","waru","iwa","tekau", // 1-10
"whero","karaka","kōwhai","kākāriki","kikorangi","poropango","papura", // ROYGBIV
"rāhina","rātū","rāapa","rāpare","rāmere","rāhoroi","rātapu" // Mon-Sun
).sortedWith(Collator.getInstance(Locale("mi_NZ")))
val groupedByLetter = words.groupBy(
{ word -> word.first()},
{word -> word.replaceFirstChar(Char::titlecase)}
).toSortedMap()
groupedByLetter shouldBe mapOf(
Pair('i', listOf("Iwa")),
Pair('k', listOf("Kākāriki", "Karaka", "Kikorangi", "Kōwhai")),
Pair('o', listOf("Ono")),
Pair('p', listOf("Papura", "Poropango")),
Pair('r', listOf("Rāapa", "Rāhina", "Rāhoroi", "Rāmere", "Rāpare", "Rātapu", "Rātū", "Rima", "Rua")),
Pair('t', listOf("Tahi", "Tekau", "Toru")),
Pair('w', listOf("Waru", "Wha", "Whero", "Whitu"))
)
}
I like that.
Primary and secondary constructors
You saw I had that wee Number class in my tests:
class Number(var value: Int, var en: String, var mi: String) {}
Initially I had written that long-form like this:
class Number {
var value = 0
var en = ""
var mi = ""
constructor(value: Int, en: String, mi: String) {
this.value = value
this.en = en
this.mi = mi
}
}
But IntelliJ admonished me with this:
And I went "um… primary and secondary constructors are a thing, are they? OK. Go for it pal". And it squizhed everything up for me.
I RTFMed and right there on the class page it says this:
A class in Kotlin can have a primary constructor and one or more secondary constructors. The primary constructor is a part of the class header, and it goes after the class name and optional type parameters.
[…]
The primary constructor cannot contain any code. …
Ah OK this makes sense if one just wants simple class with some property values (like I do here), there's no need for so much boiler plate to achieve this. Nice. I'm gonna need to read the rest of that section about initializer blocks and the like. But: tomorrow. I've messed around enough for one afternoon.
That was quite short, but it reflects the forward progress I made in a few hours exploration. The good thing is: the more I find out about Kotlin, the more I want to find out about it.
Righto.
--
Adam