Published on [Permalink]
Reading time: 17 minutes
Posted in:

Kotlin Usability

Note this blog post contains my notes from the Usability section of the book Atomic Kotlin. Consider buying a copy of the book for a more detailed reference.

Extension Functions

Extension functions add member functions to existing classes

The receiver is the type you extend

Example defining an extension function

package extensionfunctions
import atomictest.eq


fun String.singleQuote() = "'$this'"
fun String.doubleQuote() = "\"$this\""


fun main() {
  "Hi".singleQuote() eq "'Hi'"
  "Hi".doubleQuote() eq "\"Hi\""
}

You call extension functions as if they were members of the class

To use extension functions from another package you must import them

package other
import atomictest.eq
import extensionfunctions.doubleQuote
import extensionfunctions.singleQuote


fun main() {
  "Single".singleQuote() eq "'Single'"
  "Double".doubleQuote() eq "\"Double\""
}

You can access member functions or other extension functions using the this keyword. You can omit this in the same way as it can be omited inside a class.

package extensionfunctions
import atomictest.eq


// Explicit qualification
fun String.strangequote() =
  this.singleQuote().singleQuote()
  
//Implicit Qualification
fun String.tooManyQuotes() = doubleQuote().doubleQuote()


fun main() {
  "Hi".strangeQuote() eq "''Hi''"
  "Hi".tooManyQuote eq "\"\"Hi\"\""
}

Extensions to your own classes can sometimes produce simpler code

package extensionfunctions
import atomictest.eq


class Book(val title: String)


fun Book.categorize(category: String) =
  """title: "$title", category: $category"""
  
fun main() {
  Book("Dracula").categorize("Vampire") eq
    "title: Dracula, category: Vampire"
}

Extension functions can only access public members of the type being extended.

Named and Default Arguments

Example:

package color1
import atomictest.eq


fun color(red: Int, green: Int, blue: Int) =
  "($red, $green, $blue)"
  
fun main() {
  color(1, 2, 3) eq (1, 2, 3)" // [1]
  color(
    red = 76,
    green = 80,
    blue = 0
  ) eq (76, 80, 0) // [2]
  color(52, 34, blue = 0) eq (52, 34, 0) // [3]
}

[1] You would have to look at documentation
[2] The meaning of every argument is clear
[3] You aren’t required to name all args

Named arguments allow you to change the order of the colors.

You can mix named and unnamed args, but:

Default args can be specified in the function definition

package color2
import atomictest.eq


fun color(
  red: Int = 0,
  green: Int = 0,
  blue: Int = 0
) = "($red, $green, $blue)"


fun main() {
  color(139) eq (139, 0, 0)
  color(blue = 139) eq (0, 0, 139)
  color(255, 165) eq (255, 165, 0)
  color(red = 128, blue = 128) eq (128, 0, 128)
}

You can use trailing comma syntax in class constructors or function definitions

joinToString function uses default args to control prefix, postfix, and separator of the join operation on a list

Unlike toString() which may not have the behavior you want.

If you use an object as a default arg, a new instance of that object is created for each invocation.

If you pass an object instance as a default arg, then that same instance is used for each call.

If you pass the syntax for a constructor then that constructor is called every time.

Overleading

Example:

package overloading
import atomictest.eq


class Overloading {
  fun f() = 0
  fun f(n: Int) = n + 2
}


fun main() {
  val o = Overloading()
  o.f() eq 0
  o.f(3) eq 5
}

You can overload a function as long as the parameter lists differ.

You cannot overload on return types

A function signature consists of the name, paramter list, and return type

A signature also includes information about the enclosing class or receiver type

If a class has a member function with the same signature as an extension function, the member function is preferred.

However, you can overload the member function with an extension function.

package overloading
import atomictest.eq


class My {
  fun foo() = 0
}


fun My.foo() = 1
fun My.foo(i: Int) = i + 2


fun main() {
  My().foo() eq 0 // [1]
  My().foo(2) eq 4
}

[1] The extension function is never called because a member function takes precedence.

Don’t use overloading to initiate default args

The compiler calls the closest matching function first. So functions with default args take less precedence like in below:

package overladingdefaultargs
import atomictest.*


fun foo(n: Int = 99) = trace(foo-1-$n")


fun foo() {
  trace("foo-2")
  foo(14)
}


fun main() {
  foo()
  trace eq """
    foo-2
    foo-1-14
  """
}

When Expressions

When expressions are like switch statements. They compare a value against a selection of possibilities.

package whenexpressions
import atomictest.eq


val numbers = mapOf(
  1 to "eins", 2 to "zwei", 3 to "drei",
  4 to "vier", 5 to "fuenf", 6 to "sechs",
  7 to "sieben", 8 to "acht", 9 to "neun",
  10 to "zehn", 11 to "elf", 12 to "zwoelf",
  13 to "dreizehn", 14 to "vierzehn",
  15 to "fuenfzehn", 16 to "sechzehn",
  17 to "siebzehn", 18 to "achtzehn",
  19 to "neunzehn", 20 to "zwanzig"
)


fun ordinal(i: Int): String =
  when(i) {
    1 -> "erste"
    3 -> "dritte",
    7 -> "siebte"
    8 -> "achte"
    20 -> "zwanzigste"
    else -> numbers.getValue(i) + "te"
  }


fun main() {
 ordinal(2) eq "zwelte"
 ordinal(3) eq "dritte"
 ordinal(11) eq "elfte"
}

The first successful match completes execution of the when expression which becomes the return value of ordinal.

The else keyword provides a fall through when there are no other matches.

When expressions must be exhaustive.

If you treat the when expression as a statement then you can omit the else branch otherwise it is required. Unmatched values in this case are just ignored.

Another example:

package whenexpressions
import atomictest.*


class Coordinates {
  var x: Int = 0
    set(value) {
      trace("x gets $value")
      field = value
    }
    
  var y: Int = 0
    set(value) {
      trace("y gets $value")
      field = value
    }
    
  override fun toString() = "($x, $y)"
}


fun processInputs(inputs: List<String>) {
  val coordinates = Coordinates()
  for (input in inputs) {
    when (input) {
      "up", "u" -> coordinate.y-- // [1]
      "down", "d" -> coordinates.y++
      "left", "l" -> coordinates.x--
      "right", "r" -> {
        trace("Moving right")
        coordinates.x++
      } // [2]
      "nowhere" -> {}
      "exit" -> return
      else -> trace("bad input: $input")
    }
  }
}


fun main() {
  processInputs(listOf("u","d","nowhere","l","r","exit","r"))
  trace eq """
    y gets -1
    y gets 0
    x gets -1
    Moving right
    x gets 0
  """
}

[1] You can match several patterns in one branch
[2] You can use a block body

You express nothing in a branch with an empty block {}

You can return from the outer function within a branch.

Any expression can be an arg for when and the matches can be any values not just constants.

We can match a Set of values against another Set of values

package whenexpressions
import atomictest.eq


fun mixColors(first: String, second: String) =
  when (setOf(first, second)) {
    setOf("red", "blue") -> "purple"
    setOf("red", "yellow") -> "orange"
    setOf("blue", "yellow") -> "green"
    else -> "unknown"
  }
}


fun main() {
  mixColors("red", "blue") eq "purple"
}

when has a special form that takes no args. Omitting the arg means the branches can check different Boolean conditions. You can use any Boolean expression as a branch condition.

Example:

fun bmiMetricWithWhen(
  kg: Double,
  height: Double
): String {
  val bmi = kg / (height * height)
  return when {
    bmi < 10.5 -> "Underweight"
    bmi < 25 -> "Normal weight"
    else -> "Overweight"
  }
}


fun main() {
  bmiMetricWithWhen(72.57, 1.727)
}

Enumerations

An enumeration is a collection of names

Define a simple enum like so:

package enumerations
import atomictest.eq
enum class Level {
  Overflow, Hiegh, Medium, Low, Empty
}


fun main() {
  Level.Medium eq "Medium"
}

Creating an enum generates a toString() for the enum names

You must qualify each reference to an enum name.

You can omit qualification using an import to bring all names from thsi enum into the current namespace like so:

import atomictest.eq
import enumerations.Level.*


fun main() {
  Overflow eq "Overflow"
  High eq "High"
}

You can import enum values into the same file where the enum is defined

package enumerations
import atomictest.eq
import enumerations.Size.*


enum class Size {
  Tiny, Small, Medium, Large, Huge, Gigantic
}


fun main() {
  Gigantic eq "Gigantic"
  Size.values().toList() eq
    listOf(Tiny, Small, Medium, Large,
      Huge, Gigantic) // [1]
      
  Tiny.ordinal eq 0 // [2]
  Huge.ordinal eq 4
}

You can iterate over enumeration names using values.

values() returns an array so use toList() to convert to a List

The first declared constant of an enum has an ordinal value of zero.

You can perform different actions for different enum entries using a when expression like so:

package checkingoptions
import atomictest.*
import enumerations.Level
import enumerations.Level.*


fun checkLevel(level: Level) {
  when (level) {
    Overflow -> trace(">>> Overflow"
    Empty -> trace("Alert: Empty")
    else -> trace("Level $level OK")
  }
}


fun main() {
  checkLevel(Empty)
  checkLevel(Low)
  checkLevel(Overflow)
  
  trace eq """
    Alert: Empty
    Level low OK
    >>> Overflow
  """
}

An enum is a class with a fixed number of instances

You can add member properties and functions just as in a regular class. You must use semicolon after the last enum value like so:

package enumerations
import atomictest.eq
import enumerations.Direction.*


enum class Direction(val notation: String) {
  North("N"), South("S"),
  East("E"), West("W");
  
  val opposite: Direction
    get() = when(this) {
      North -> South
      South -> North
      East -> West
      West -> East
    }
}


fun main() {
  North.notation eq "N"
  North.opposite eq Sout
  West.opposite.opposite eq West
  North.opposite.notation eq "S"
}

The Direction class contains a notation and opposite property.

Notation property is passed as a constructor parameter

Opposite is a computed property

Data Classes

Data classes can simplify code for classes that are essentially a data holder.

Define a data class like so:

package dataclasses
import atomictest.eq


data class Siimple(
  val arg1: String,
  val arg2: Int
)


fun main() {
  val s1 = Simple("hi", 1)
  val s2 = Simple("Hi", 1)
  s1 eq "Simple(arg1=Hi, arg2=1)"
  s1 eq s2
}

In the above example toString defaults to a readable output.

Two instances of the same data class containing identical data are equal by default. With a regular class you have to define a equals() function.

You can use the copy function with data classes like so:

package dataclasses
import atomictest.eq


data class DetailedContact(
  val name: String,
  val surname: String,
  val number: String,
  val address: String
)


fun main() {
  val contact = DetailedContact(
    "Miffy",
    "Miller",
    "1-234-567890",
    "123 Main St"
  )
  
  val newContact = contact.copy(
    number = "1-098-765432",
    address = "321 Main St"
  )
  newContact eq DetailedContact(
    "Miffy"
    "Miller"
    "1-098-765432",
    "321 Main St")
}

The copy() function parameters are identical to the constructor parameters. All args have default values equal to current values.

Data classes generate an appropriate hash function so that objects can be keys in HashMaps and HashSets

package dataclasses
import atomictest.eq


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


fun main() {
  val korvo = Key("Korvo", 19)
  korvo.hashCode() eq -2041757108
  val map = HashMap<Key, String>()
  map[korvo] = "Alien"
  map[korvo] eq "Alien"
  val set = HashSet<Key>()
  set.add(korvo)
  set.contains(korvo) eq true
}

Destructuring Declarations

Use the Pair class to return two values

package destructuring
import atomictest.eq


fun compute(input: Int): Pair<Int, String> =
  if (input > 5)
    Pair(input * 2, "High")
  else
    Pair(input * 2, "Low")


fun main() {
  compute(7) eq Pair(14, "High")
  val result = compute(5)
  result.first eq 10
  result.second eq "Low"
}

Pair is a parameterized type such as List or Set

You can declare and initialize several identifiers using a destructuring declaration:

val (a, b, c) = composedValue

This destructures a composed value and positionally assigns its components

There is a Triple class to return three values.

For more than three values use a class.

Data classes automatically allow destructuring like so:

package destructuring
import atomictest.eq


data class Computation(
  val data: Int,
  val info: String
)


fun evaluate(input: Int): Computation {
  if (input > 5)
    Computation(input * 2, "High")
  else
    Computation(input * 2, "Low")
}


fun main() {
  val (value, description) = evaluate(7)
  value eq 14
  description eq "High"
}

Again this destructuring is position dependent.

If you do not need some identifiers, you can use underscores instead of their names. Or omit them completely if they appear at the end.

You can use a for loop to iterate over a Map or List of destructurable elements

import atomictest.eq


fun main() {
  result = ""
  val map = mapOf(1 to "one", 2 to "two")
  
  for ((key, value) in map) {
    result += "$key = $value, "
  }
  result eq "1 = one, 2 = two, "
  
  result = ""
  val listOfPairs = listOf(
    Pair(1, "one"), Pair(2,"two"))
  
  for ((i, s) in listOfPairs) {
    result += "($i, $s), "
  }
  result eq "(1, one), (2, two), "
}

Destructuring cannot be used to create class properties. It can only be used for local vals and vars.

Nullable Types

Null can represent no value like when retrieving a value from a map:

import atomictest.eq


fun main() {
  val map = mapOf(0 to "yes", 1 to "no")
  map[2] eq null
}

If you don’t handle a null value in Java correctly, you can get a NullPointerEception.

In Kotlin types default to non-nullable. If something can produce a null result you must opt in like so:

val s: String? = null
val s: String? = "abc"

You cannot assign an identifier of a nullable type to an identifier of a non-nullable type.

Nullable types are different types all together.

In Kotlin, you can’t simply dereference a member function or property a nullable identifier like so:

import atomictest.eq


fun main() {
  val s1: String = "abc"
  val s2: String? = s1
  
  s1.length eq 3
  // Doesn't compile
  // s2.length
}

This is because values of most types are stored as references to objects in memory.

You can instead check if the reference is not null like so:

import atomictest.eq


fun main() {
  val s: String? = "abc"
  if (s != null)
    s.length eq 3
}

First checking for null value enables you to dereference it a nullable.

Kotlin automatically creates nullable and non-nullable types when you create a new class.

Safe Calls & the Elvis Operator

Only safe (?. ) or non-null asserted (!!.) calls are allowed on a nullable receiver.

A safe call replaces the dot syntax in a regular call with ?.

Safe calls access members of a nullable type in a way that ensures no exceptions are thrown. They only perform an operation when the receiver is not null.

package safecalls
import atomictest.*


fun String.echo() {
  trace(uppercase())
  trace(this)
  trace(lowercase())
}


fun main() {
  val s1: String? = "Howdy"
  s1?.echo()
  val s2: String? = null
  s2?.echo()
  trace eq """
    HOWDY
    Howdy
    howdy
  """
}

The elvis operator (?:) acts as a nil coalescing operator.

import atomictest.eq


fun main() {
  val s1: String? = "abc"
  (s1 ?: "----") eq "abc"


  val s2: String? = null
  (s2 ?: "----") eq "----"
}

The elvis operator is equivalent to:

if (s1 != null) s1 else null

The elvis operator is typically used after a safe call to produce a meaningful value instead of the default null

package safecalls
import atomictest.eq


fun checkLength(s: String?, expected: Int) {
  val length1 =
    if (s != null) s.length else 0
  val length2 = s?.length ?: 0
  length1 eq expected
  length2 eq expected
}


fun main() {
  checkLength("abc", 3)
  checkLength(null, 0)
}

Safe calls allow you to chain calls concisely:

package safecalls
import atomictest.eq


class Person(
  val name: String,
  val friend: Person? = null
)


fun main() {
  val alice = Person("Alice")
  alice.friend?.friend?.name eq null
  
  val bob = Person("Bob")
  val charlie = Person("Charlie", bob)
  bob.friend = charlie
  bob.friend?.friend?.name eq "Bob"
  
  (alice.friend?.friend?.name ?: "Unknown") eq "Unknown"
}

When you chain several members using safe calls the result is null if any intermediate expression is null.

Non-Null Assertions

A second approach to the problem of nullable types is to have knowledge that the reference in question isn’t null.

To make the claim use the !! (non-null assertion operator).

import atomictest.eq


fun main() {
  var x: String? = "abc"
  x!! eq "abc"
  x = null
  capture {
    val s: String = x!! 
  } eq "NullPointerException"
}

Typically you use !! with a dereference:

import atomictest.eq


fun main() {
  val s: String? = "abc"
  s!!.length eq 3
}

With a map you can use getValue() to throw a NoElementException if a key is missing

Extensions for Nullable Types

The kotlin standard library provides Strings extension functions including

import atmoictest.eq


fun main() {
  val s1: String? = null
  s1.isNullOrEmpty() eq true
  
  val s2 = ""
  s2.isNullOrBlank() eq true
  
  val s3: String = "\t\n"
  s3.isNullOrEmpty() eq false
  s3.isNullOrBlank() eq true
}

In the above example the function names suggest they are for nullable types however you can call the functions on s1 without a safe call.

This is because these are extension functions on the nullable type String?

We can rewrite isNullOrEmpty() as a non-extension function that takes the nullable String as a parameter

package nullableextensions
import atomictest.eq


fun isNullOrEmpty(s: String?): Boolean =
  s == null || s.isEmpty()
  
fun main() {
  isNullOrEmpty(null) eq true
  isNullOrEmpty("") eq true
}

Because s is nullable we explicitly check for null or empty

Now rewriting as an extension to the nullable type

package nullableextensions
import atomictest.eq


fun String?.isNullOrEmpty(): Boolean =
  this == null || isEmpty()

Introduction to Generics

Generics create parameterized types

Initial motivation for generics included creating collection classes like List, Set, or Map.

Non-generic class:

package introgenerics
import atomictest.eq


data class Automobile(val brand: String)


class RigidHolder(private val a: Automobile) {
  fun getValue() = a
}


fun main() {
  val holder = RigidHolder(Automobile("BMW"))
  holder.getValue() eq "Automobile(brand=BMW)"
}

Now we define a generic holder with a Type Parameter

package introgenerics
import atomictest.eq


class GenericHolder<T>(
  private val value: T
) {
  fun getValue(): T = value
}


fun main() {
  val h1 = GenericHolder(Automobile("Ford"))
  val a: Automobile = h1.getValue()
  a eq "Automobile(brand=Ford)"
  
  val h2 = GenericHolder(1)
  val i = h2.getValue()
  i eq 1
  
  val h3 = GenericHolder("Hello")
  val s = h3.getValue()
  s eq "Hello"
}

In Kotlin there is a universal type called Any.

Any allows any type of argument

The issue is that when you need the specific type, you lose track of the original type:

package introgenerics
import atomictest.eq


class AnyHolder(private val value: Any) {
  fun getValue() = value
}
  
class Dog {
  fun bark() = "Ruff!"
}
  
fun main() {
  val holder = AnyHolder(Dog())
  val any = holder.getValue()
    //Doesn't Compile
    //any.bark()
    
  val genericHolder = GenericHolder(Dog())
  val dog = genericHOlder.getValue()
  dog.bark() eq "Ruff!"
  }

Defining a generic function

package introgenerics
import atomictest.eq


fun <T> identity(arg: T) = arg


fun main() {
  identity("Yellow") eq "Yellow"
}

Defining a generic extension function:

package introgenerics
import atomictest.eq


fun <T> List<T>.first(): T {
  if (isEmpty())
    throw NoSuchElementException("Empty List")
  return this[0]
}


fun <T> List<T>.firstOrNull() =
  if (isEmpty()) null else this[0]
  
fun main() {
  listOf(1, 2, 3).first() eq 1
  
  val i: Int? = listOf(1, 2, 3).firstOrNull() // [1]
  i eq 1
  
  val s: String? eq listOf<String>().firstOrNull() // [2]
  s eq null
}

Extension Properties

Properties can be extension properties

Example:

package extensionProperties
import atomictest.eq


val String.indices: IntRange
  get() = 0 until length


fun main() {
  "abc".indices eq 0..2
}

Any extension function without parameters could be converted into a property. Choose whatever is more readable. The Kotlin style guide prefers a function if the function throws an exception.

Defining a generic extension property:

package extensionproperties
import atomictest.eq


val <T> List<T>.firstOrNull: T?
  get() = if (isEmpty()) null else this[0]


fun main() {
  listOf(1, 2, 3).firstOrNull eq 1
  listOf<String>().firstOrNull eq null
}

When the generic arg type isn’t used you may replace it with a star projection (*)

package extensionproperties
import atomictest.eq


val List<*>.indices: IntRange
  get() = 0 until size


fun main() {
  listOf(1).indices eq 0..0
  listOf('a', 'b', 'c').indices eq 0..2
  listOf<Int>().indices eq IntRange.EMPTY
}

Using a star projection you lose all type information.

In the below example you can only assign an element of List<*> to Any? because we do not know if the type is nullable.

import atomictest.eq


fun main() {
  val list: List<*> = listOf(1, 2)
  val any: Any? = list[0]
  any eq 1 // [Works inside testing framework]
}

Break and Continue

Break and continue abandon the remainder of the code in a loop.

And break to the end or continue at the beginning of the next iteration.

Break and continue are only available within the looping constructs for, while, and do-while.

Example:

import atomictest.eq


fun main() {
  val nums = mutableListOf(0)
  for (i in 4 until 100 step 4) {
    if (i == 8) continue
    if (i == 40) break
  }
  
  nums eq "[0, 4, 12, 16, 20, 24, 28, 32, 36]"
}

We could rewrite as a while loop:

import atomictest.eq


fun main() {
  val nums = mutableListOf(0)
  val i = 0
  while (i < 100) {
    i += 4
    if (i -- 8) continue
    if (i == 40) break
    nums.add(i)
  }
  nums eq "[0, 4, 12, 16, 20, 24, 28, 32, 36]"
}

Or do-while

import atomictest.eq


fun main() {
  val nums = mutableLIstOf(0)
  var i = 0
  
  do {
    i += 4
    if (i == 8) continue
    if (i -- 40) break
  } while (i < 100)
  nums eq "[0, 4, 12, 16, 20, 24, 28, 32, 36]"
}

Labels allow break and continue to jump to the boundaries of enclosing loops

Example:

import atomictest.eq


fun main() {
  val strings = mutableListOf<String>()
  outer@ for (c in 'a'..'e') {
    for (i in 1..9) {
      if (i == 5) continue@outer
      if ("$c$i == "c3") break@outer
      strings.add("$c$i")
    }
  }
  strings eq listOf("a1", "a2", "a3", "a4",
    "b1", "b2", "b3", "b4", "c1", "c2")
}

They also work with while or do while