Kotlin’s killer features

De kracht van het Java platform is dat je broncode van andere talen dan Java kunt compileren naar universele bytecode die de JVM kan uitvoeren. Groovy en Scala zijn verreweg de bekendste en sinds kort is er Kotlin. Ontwikkeling begon al in 2011 bij Jetbrains en versie 1.0 zag in 2016 het levenslicht. Inmiddels is Kotlin een Apache 2 open source project en tijdens de Google I/O keynote afgelopen mei kreeg het de status als een van de hoofdtalen voor het Android platform: een belangrijke erkenning.

Jasper Sprengers

 

Het voordeel van alternatieve JVM talen is dat er al een robuust Java ecosysteem bestaat waar je code gebruik van kan maken. Denk hierbij aan applicatieservers, database drivers en allerhande externe libraries en frameworks, bijvoorbeeld voor testen. Zelfs verschillende JVM talen in hetzelfde project compileren met Maven en Gradle is mogelijk. Kotlin is voor ervaren Javanen snel te leren. De syntax is niet radicaal anders en heeft veel weg van Scala.

In de beperkte ruimte van dit artikel zal ik geen stoomcursus Kotlin proberen te geven maar mij beperken tot wat ik zelf de krachtigste eigenschappen vind. Maar voordat we in detail naar deze killing features gaan kijken vraag je je misschien terecht af waarom we eigenlijk een nieuwe taal voor de JVM nodig hebben. Wat is er mis met Java?

Oude Java broncode blijft backwards compatible met hogere versies van de JVM en externe libraries. Dit is fijn voor de levensduur van legacy systemen maar het betekent ook dat de taal in syntax niet ingrijpend kan evolueren. Nieuwe JVM talen voorzien wel in de behoefte aan meer innovatieve taalfeatures en kunnen breken met minder geslaagde keuzes uit de jaren negentig, zoals checked exceptions (zowel de Scala, Groovy als Kotlin compilers dwingen niet meer af dat je deze moet afvangen). Overigens blijft Java nog steeds onverminderd populair in aantallen vacatures vergeleken bij zijn kleine zusjes. Ook Kotlin zal niet op korte termijn een Java killer worden, al lopen de early adopters – zoals ikzelf! – over van bekeringsdrang op blogs en social media.

Regelmatig een nieuw framework of programmeertaal leren is goed voor je ontwikkeling en ook noodzakelijk voor je inzetbaarheid op de lange termijn. Maar of het nu al commercieel verantwoord is om hele teams naar Kotlin te laten overstappen is een andere vraag. Daarin speelt de leergierigheid van je team een grote rol. Bedenk dat je begint vanuit achterstand en onderschat de tijd niet die het kost om met zijn allen je oude niveau van productiviteit te evenaren, laat staan verbeteren. Daarnaast heeft een programmeertaal een robuust en volwassen ecosysteem nodig om maximaal productief te kunnen zijn. Denk hierbij aan externe libraries, middleware en developer tools, maar ook aan documentatie (boeken, Q&A forums) en ervaren professionals om in te huren als de gratis digitale vraagbaak niet voldoet. In deze opzichten heeft Kotlin nog wel een weg te gaan. Het zal je niet verbazen dat IDE integratie met Jetbrains eigen IntelliJ prima is, maar met name code validatie en auto-completion zijn nog niet zo uitgebreid als voor Java. Integratie met Spring(Boot) is goed, maar heeft hier en daar nog wat lijmcode nodig. Genoeg gewaarschuwd: laat ik een korte demonstratie geven van hoe Kotlin jou kan helpen leesbaarder en robuuster te coderen.

Met stip op nummer één staat ingebouwde null-safety. Kotlin rekent radicaal af met de NullPointerException. Referenties naar null zijn in principe verboden tenzij je in de declaratie van variabele, parameter of return type expliciet aangeeft dat het wel mag. Dan verplicht de compiler je dit netjes af te vangen.

 

val felix: Cat = null

val felix = Cat()

var felixOrNull: Cat? = null

 

Regel 1 compileert niet, want Cat is standaard non-nullable. De niet-muteerbare variabele felix (aangeduid met val) in regel 2 verwijst nu naar een nieuw Cat object (Kotlin kent geen new keyword). Je mag het type in de declaratie weglaten omdat de compiler dat kan afleiden.

felixOrNull in regel 3 mag wel null zijn want het is van type Cat?. Muteerbare variabelen geef je aan met var.

Nullability geldt ook voor return types. De volgende functie retourneert een Customer object, of null. fun is het keyword voor functies. Return type Customer? volgt na de parameter lijst.

 

fun findCustomer(id: Int): Customer? {… }

val customerOrNone: Customer? = findCustomer(123)
val customer: Customer = findCustomer(123) ?: throw IllegalArgumentException(“No customer 123”)

 

Met de zogenaamde elvis notatie ?. kunnen we de null afvangen en een exception gooien. Aanroepen van methodes op een nullable referentie zoals customerOrNone.name zal niet compileren. name is overigens een getter functie, maar dan zonder get prefix. We moeten deze getter conditioneel aanroepen: nameorNull: String? = customerOrNone?.name

De aanroep ?.name levert een null op die je weer kan converteren naar een default waarde:
name: String = customerOrNone?.name ?: “No name”

 

Laten we naar een uitgebreider voorbeeld kijken van Kotlin classes, constructors en functie-aanroepen.

 

1: class Employee(val name: String, val salary: Int)

2: class Department(val name: String, var employees: List<Employee> = listOf()) {

3:      var manager: Employee? = null

4:           get() = if (field == null && !employees.isEmpty()) employees[0] else field

5:           set(value) {

6:              if (value == null) throw IllegalArgumentException(“We need a real manager”)

7:           }

8: }

9:   val it = Department(name = “IT”)

10: println(it.manager ?: “No boss!”)

11: it.employees = listOf(Employee(“Moss”, 4000), Employee(“Roy”, 3500))

12: println(it.manager!!.name)

13: it.manager = Employee(“Jenn”, 5000)

14: println(it.manager!!.name)

15: fun printSalary(salary: Int, name: String) = “$name earns $salary per month”

16: println(it.employees.map { it.name }.reduce { acc, s -> “$acc and  $s” })
17: println(it.employees.maxBy { it.salary }?.name)

18: it.employees.forEach { println(printSalary(it.salary, it.name)) }

 

Regel 1 definieert een klasse Employee met properties name en salary. De code tussen haakjes vormt de default constructor. Het val keyword kenmerkt deze als niet-muteerbare properties. Java primitives zijn in Kotlin volwaardige immutable objecten (Int, Long, Double, Boolean). Voor val properties maakt de compiler getters en voor vars getters en setters. De employees property in regel 2 is muteerbaar en wordt standaard gezet op een lege immutable List m.b.v. de listOf utility functie. Dit betekent dat je dit argument weg mag laten in de aanroep van de constructor (zie r.9).

 

Regels 3-7: manager is een nullable property met eigen getter en setter. field verwijst naar de huidige waarde van de property. If/else fungeert in Kotlin als een expressie met een return waarde waarvan de compiler zelf het type kan bepalen. Accolades en het return keyword mag je weglaten in de body van de one-liner. employees[0] is kort voor employees.get(0).

Regels 9-13:  hier maken we een nieuw Department object. Kotlin kent geen new keyword. Je mag de argumenten bij naam noemen, maar dit is niet verplicht. println en listOf() zijn standaard utility functies waarvoor geen speciale import vereist is. We hebben manager inmiddels naar een non-null waarde gezet (r.13) en met de !! notatie vertel je de compiler dat deze property nooit null zou mogen zijn. Wees voorzichtig met !!, want hiermee zijn nulpointers nog steeds mogelijk. Met een Java-oog lijkt het zetten van nieuwe waardes voor employees en manager alsof we direct publieke properties manipuleren, maar we roepen wel degelijk setter methodes aan.

 

In regels 15-18 zie je de compacte kracht van lambda’s en functies binnen functies (printSalary). Merk de dollar notatie op voor geformatteerde output. De map functie verwacht een lambda met één argument. In dat geval mag je hiernaar verwijzen met it. Volledig uitgeschreven is het { employee: Employee -> employee.name }. De return waarde van de maxBy functie is een Employee?, vandaar de elvis notatie. Als maxBy null teruggeeft zal de name getter niet worden aangeroepen.

 

Kotlin en Scala zijn erg goed in ruimtebesparende maatregelen. Wat de compiler kan afleiden hoe je niet zelf te typen. Dit zie je het sterkst in type inference, iets wat Java maar zeer mondjesmaat toestaat. Toch moet je sommige features met beleid gebruiken. Ze leiden wel tot bondigere code, maar kunnen ook verwarring zaaien. Operator overloading en extension methods zijn treffende voorbeelden.

 

class Dollar(val cents: Int) {

        operator fun plus(cts: Int): Dollar = Dollar(cts + cents)

        operator fun plus(money: Dollar): Dollar = Dollar(cents + money.cents)
}

 

Met operator fun plus bombarderen we het plus teken tot volwaardige functie. Nu kunnen we Dollar(10) + Dollar(20) schrijven. Echt interessant wordt het pas als we aan de Int class een methode toevoegen om Dollar objecten mee op te kunnen tellen. In Kotlin kun je platform classes zoals Int via zogenaamde method extension toch uitbreiden:

 

operator fun Int.plus(money: Dollar): Dollar = money.plus(this)

 

Nu kunnen we het volgende:

 

val p1 = Dollar(1200)

val p2 = Dollar(800)

println(p1 + p2 + 3) // p1.plus(p2).plus(3)

println(3 + p1 + p2) // 3.plus(p1).plus(p2)

val totalPrice = 3 + currentPrice() + orderTotal()

 

Maar wat is nu het type van totalPrice? De compiler weet het, maar jij ziet niet meteen dat we Int hebben uitgebreid. In potentie scheppen extension methods meer verwarring dan dat ze je code verhelderen. Null-safe types dwingen je daarentegen om instabiele situaties correct af te handelen. Hierdoor zul je null referenties veel minder vaak willen gebruiken. Een andere categorie zijn features die fundamentele concepten op een intuïtieve manier implementeren, zoals invariance, contravariance en covariance. Hoe zat dat ook weer?

In Java mag je een List<Apple> niet toewijzen aan een List<Fruit>, ook al is Apple een subklasse van Fruit. Deze zijn namelijk invariant. Een zak appels mag je best als een zak fruit beschouwen, zolang je er maar geen bananen in stopt. Evenzeer mag je een universele fruitpers een appelpers noemen: dat noemen we contravariance. Kotlin generics zijn standaard invariant, maar met de in en out modifiers wordt dat rekkelijk. Een fruitig voorbeeld:

 

interface Juicer<in F : Fruit, out J : Juice> {

        fun squeeze(f: F): J

}

We hebben een pers waar een subklasse van Fruit ingaat en een subklasse van Juice uitkomt. De modifiers dwingen af dat J alleen als return type wordt gebruikt en F alleen voor functieparameters. Dit geeft vrijheid in het typeren van implementaties. Stel dat een AppleJuicer implementatie van appels appelsap maakt:

 

val appleJuicer: Juicer<Apple, AppleJuice> = AppleJuicer()

val juice: AppleJuice = appleJuicer.squeeze(Apple())

 

Dankzij de out modifier is Juice nu covariant en zal het volgende goed compileren: val appleInFruitJuiceOut: Juicer<Apple, Juice> = appleJuicer.

De SuperJuicer maakt sap van elk soort fruit: val fruitJuicer: Juicer<Fruit, Juice> = SuperJuicer()

Deze universele pers is dus ook een appelpers. Dankzij de in modifier is Fruit contravariant en mogen we hem ook als volgt typeren: val applesInFruitJuiceOut: Juicer<Apple, Juice> = fruitJuicer

 

Er zijn nog meer nuttige features en ontwikkelingen om ter afsluiting aan te stippen.

 

  • meerdere publieke klassen mogen in één source file staan.
  • Kotlin kent geen static keyword, maar wel object. Hiermee maak je klassen met maar één instantie (singletons).
  • Zoals we al zagen zijn functies first-class citizens. Ze mogen zelfs in een aparte source file staan, d.w.z. niet als member van een class. Dit maakt een meer functionele stijl van programmeren mogelijk.
  • Er is ook een Kotlin naar JavaScript compiler en met het nog prille kotlin-native project kun je zelfs native code (zonder JVM) genereren voor bijvoorbeeld IOT-devices.

 

Het Kotlin team heeft een indrukwekkende prestatie geleverd. Leergemak en productiviteit is hun filosofie en hierin onderscheiden ze zich bewust van Scala. Deze taal is weliswaar rijker aan features en daardoor expressiever, maar ook een stuk lastiger onder de knie te krijgen op expert niveau. “If you are happy with Scala you most likely do not need Kotlin” geven ze eerlijk toe. Voor mij geldt het omgekeerde inmiddels ook.

 

Referenties:

Je startpunt voor alles over Kotlin: http://kotlinlang.org/

Maven project met code uit dit artikel: https://gitlab.com/jsprengers/kotlin-demo