https://sintheta.nexus/blog/feed.xml

Kotlin Tour

2024-06-30

Preamble

Heard positive things about Kotlin, and it seems practical for UI applications targeting Linux, Windows & Android (on which it's the first class language). (.NET seems an interesting competitor for the aforementioned use cases).

Kotlin definitely marks some positive checkboxes:

  • statically typed
  • type inference
  • less verbose than Java
  • can compile to machine code
  • cross-platform
  • sizeable ecosystem
    • if you consider Java libraries
      • which though, if you use them, make Kotlin-native more difficult to use

Some of my favorite, albeit basic, language features based on having worked through the Kotlin Tour (and what follows isn't really much more than a concise pick-and-choose from the tour):

Highlights from the Kotlin Tour

string template expressions

Maybe this is something only someone hurt by C++ may find neat. Yes, std::format or in my case fmt::format in C++ is what I relied on anyways, but to have a nice syntax by default is nice.

val v = 10
println("$v + 1 = ${v + 1}") // prints: 10 + 1 = 11

read-only views of mutable Data structures

val list = mutableListOf(1, 2, 3)
val listLocked : List<Int> = list // read-only view of list

conditional expressions

if-else may be used as an expression and returns a value, same w/ when (which is similar to switch in C-like languages). Instead of ternary ?: from C you can do:

val max = if (a > b) a else b

Conditional expressions especially come in handy together with single expression functions and lambda functions!

single expression functions & nested functions

fun main()
{
	fun add0(a : Int, b : Int) : Int { return a + b }
	fun add1(a : Int, b : Int) : Int { a + b }  // implicit return
	fun add2(a : Int, b : Int) = a + b  // single expression function

	println(add0(1, 2)) // 3
	println(add1(1, 2)) // 3
	println(add2(1, 2)) // 3
}

In a single-expression function

  • we use =
  • the return statement is implicit
  • the return type is inferred

As you can also see we can freely nest functions as well.

lambda expressions

A lambda expression is an anonymous function which can be:

  • a) invoked on its own
  • b) assigned to a variable
  • c) passed as an argument
  • d) returned from a function

Its syntax: { parameters -> function body } (arguments)

a) invoked on its own

val sum = { a : Int, b : Int -> a + b } (1, 2)	        // 3
val sum = { a : Int, b : Int -> a + b }.invoke(1, 2)	// 3

b) assigned to a variable

val add = { a : Int, b : Int -> a + b }
println(add(2, 3)) // 5

c) passed as an argument

val retrievePositives = { z : Int -> z > 0 }
val retrieveNegatives = { z : Int -> z < 0 }

val l = listOf(-3, -2, -1, 0, 1, 2, 3)

println(l.filter(retrievePositives)) // [1, 2, 3]
println(l.filter(retrieveNegatives)) // [-3, -2, -1]

d) returned from a function

And this is where we combine a few things to create a neat example.

fun toSeconds(time : String) : (Int) -> Int = when (time)
{
	"hour"		-> { value -> value * 60 * 60 }
	"minute"	-> { value -> value * 60 }
	"second"	-> { value -> value }
	else		-> { value -> value }
}

val listInMinutes = listOf(2, 10, 3, 15)

println(
	"Minutes list, total seconds: "
	+ listInMinutes.map(toSeconds("minute")).sum()
)

toSeconds is a function which returns a lambda function (Int) -> Int. As said before we have conditional expressions, and use when here as such to return different lambda functions based on the time String passed. A fact which we use then to pass this lambda function to the map method of our list.

trailing lambdas

This is some syntactic sugar:

  • a) if lambda function is only argument, don't need to wrap in {}
  • b) if lambda function is last argument, can be placed outside of ()

Little niceties like this add up.

lambda expression example

An example from on of the tour's exercises which I found neat code:

val actions = listOf("title", "year", "author")
val prefix  = "https://example.com/book-info"
val id      = 5
val urls    = actions.map { action -> "${prefix}/${id}/${action}" }

println(urls)

For every action in actions we create a URL out of the prefix and fixed id. The resulting urls list will be:

[
 https://example.com/book-info/5/title,
 https://example.com/book-info/5/year,
 https://example.com/book-info/5/author
]

classes

They behave mostly as you'd expect, but there's a separation of a class and data class. The latter being nothing more than a class with pre-defined member functions, making them useful for data storage.

data class User(val name : String, val id : Int)

val user = User("John", 1)

println(user)			// User(name=John, id=1)
val user2 = user.copy(id = 2)	// second John w/ id 2
println(user2)			// User(name=John, id=2)
println(user == user2)  	// false

null safety

For a type to be potentially nullable it has to be declared as such using the ? operator. The safe call operator is ?. and the Elvis operator (which lets you specify a default in case of null) is ?:

nullable type via ?

var maybeNull : String? = "Hello"
maybeNull = null // legal

safe call operator ?.

val nullString : String? = null
println(nullString?.uppercase()) // null

since nullString is null the uppercase method won't be called and null is returned. Can even be chained: obj?.method1()?.method2()?.method3()

Elvis operator ?:

fun stringLength(s : String?) : Int = s?.length ?: 0
println(stringLength(null)) // 0

In case s?.length is null we will simply return 0. Since we've defined the Elvis operator we can also return Int from our function instead of Int? (we'll never return null after all).

Downsides

Potential downsides of Kotlin: JVM reliance, Kotlin-native and Kotlin multiplatform being relatively new (albeit I can't judge at this point how severe a downside this might be), IDE reliance (have fun writing one off kotlin programs in anything not a full fledged IDE or without an IDE's typical project initialization), Gradle (never seemed particularly straight forward to me), reliance on Java libraries (obviously not everything is available as Kotlin native libraries, making Kotlin multiplatform less of an attractive proposition)...

And in typical VM-language fashion: things I'd like to know being abstracted away. I'm sure the experienced Kotlin developer knows exactly when a data structure is copied or a reference is created et cetera, but it's not particularly explicit in code, nor, so my suspicion, can it even be explicitly controlled in some cases. Memory handling is partly JVM magic, simple as that.

On readibility I'm not sure. If you're not accustomed to functional programming and maybe return to some Kotlin code after a long while, I can imagine it might not be super straightforward.

Conclusion

It is however a concise & expressive language w/ some noteable niceties, and some interesting use cases (native on Android, compilable on Linux, somewhat cross-platform, ...). Things to keep in mind.