Kotlin Tour
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
- if you consider Java libraries
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.