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

Kotlin Power Tools

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

Extension Lambdas

An extension lambda is like an extension function. It defines a lambda instead of a function.

Example:

package extensionlambdas
import atomictest.eq

val va (String, Int) -> String = { str, n ->
  str.repeat(n) + str.repeat(n)
}

val vb: String.(Int) -> String = {
  this.repeat(it) + repeat(it)
}

fun main() {
  va("Vanbo", 2) eq
    "VanboVanboVanboVanbo"
  vb("vanbo", 2) eq
    "VanboVanboVanboVanbo"
  //"Vanbo.v(2) doesn't compile
  "Vanbo".vb(2) eq
    "VanboVanboVanboVanbo"
}

Extension lambdas can appear as function parameters such as in f2():

package extensionlambdas

class A {
  fun af() = 1
}

class B {
  fun bf() = 2
}

fun f1(lambda: (A, B) -> Int) =
  lambda(A(), B())

fun f2(lambda: A.(B) -> Int) =
  A().lmbda(B())
  
fun lambdas() {
  f1 { aa, bb -> aa.af() + bb.bf() }
  f2 { af() + it.bf() }
}

If an extension lambda returns Unit, the result produced by the lambda body is ignored:

package extensionlambdas

fun unitReturn(lambda A.() -> Unit {
  A().lambda()
}

fun nonUnitReturn(lambda: A.() -> String) =
  A().lambda()
  
fun lambdaUnitReturn() {
  unitReturn {
    "Unit ignores the return value" +
    "So it can be anything"
  }
  unitReturn { 1 }
  unitReturn { }
  nonUnitReturn {
    "Must return the proper type"
  }
  //nonUnitReturn { } // Not an Option
}

You can pass a function reference where an extension lambda is expected:

package extensionlambdas
import atomictest.eq

fun Int.d1(f: (Int) -> Int) = f(this) * 10

fun Int.d2(f: Int.() -> Int) = f() * 10

fun f1(n: Int) = n + 3
fun Int.f2() = this + 3

fun main() {
  74.d1(::f1) eq 770
  74.d2(::f1) eq 770
  74.d1(Int::f2) eq 770
  74.d2(Int::f2) eq 770
}

Polymorphism works with both ordinary extension functions and extension lambdas

package extensionlambdas

open class Base {
  open func f() = 1
}

class Derived: Base() {
  override fun f() = 99
}

fun Base.g() = f()
fun Base.h(x1: Base.() -> Int) = x1()

fun main() {
  val b: Base = Derived() //Upcast
  b.g() eq 99
  b.h { f() } eq 99
}

You can use anonymous function syntax instead of extension lambdas:

package extensionlambdas
import atomoictest.eq

fun exec(
  arg1: Int, arg2: Int,
  f: Int.(Int) -> Boolean
) = arg1.f(arg)

fun main() {
  exec(10, 2, fun Int.(d: Int): Boolean {
    return this % d == 0
  }) eq true
}

Builder APIs use extension lambda syntax extensively

Example:

package extensionfunctions
import atomictest.eq

private fun messy(): String {
  val built = StringBuilder() // [1]
  built.append("ABCs: ")
  ('a'..'z').forEach { built.append(it) }
  return built.toString()
}

private fun clean() = buildString {
  append("ABCs: ")
  ('a'..'z').forEach { append(it) }
}

private fun cleaner() =
  ('a'..'z').joinToString("", "ABCs: ")
  
fun main() {
  messy() eq "ABCs: abcdefghijklmnopqrstuvwxyz"
  messy() eq clean()
  clean() eq cleaner()
}

[1] we repeat the name built multiple times and createa a StringBuilder

In clean we use buildString() without needing to create and manage the receiver for append()

There are similar functions such as buildList() and buildMap()

Benefits of the builder pattern:

Example of using extension lambdas in practice:

package sandwich
import atomictest.eq

open class Recipe: ArrayList<RecipeUnit>()

open class RecipeUnit {
  override fun toString() =
    "${this::class.simpleName}"
}

open class Operation: RecipeUnit()
class Toast: Operation()
class Grill: Operation()
class Cut: Operation()

open class Ingredient: RecipeUnit()
class Bread: Ingredient()
class PeanutButter: Ingredient()
class GrapeJelly: Ingredient()
class Ham: Ingredient()
class Swiss: Ingredient()
class Mustard: Ingredient()

open class Sandwich: Recipe() {
  fun action(op: Operation): Sandwich {
    add(op)
    return this
  }
  fun grill() = action(Grill())
  fun toast() = action(Toast())
  fun cut() = action(Cut())
}

fun sandwich(
  fillings: Sandwich.() -> Unit
): Sandwich {
  val sandwich = Sandwich()
  sandwich.add(Bread())
  sandwich.toast()
  sandwich.fillings()
  sandwich.cut()
  return sandwich
}

fun main() {
  val pbj = sandwich {
    add(PeanutButter())
    add(GrapeJelly())
  }
  pbj eq "[Bread, Toast, PeanutButter, " +
    "GrapeJelly, Cut]"
}

The fillings extension lambda allows the caller to configure the Sandwich in numerous ways without requiring a constructor for each configuration.

Scope Functions

Scope functions create a temporary scope wherin you can access an object without using its nae.

Scope functions are purely for code conciseness and readability. They do not provide anything additional.

There are five scope functions:

  1. let
  2. run
  3. with
  4. apply
  5. also

They are designed to work with a lambda and do not require an import. They differ in how you access the context object using either it or this and in what they return.

with() uses a different calling syntax than the others:

package scopefunctions
import atomictest.eq

data class Tag(var n: Int = 0) {
  var s: String = ""
  fun increment() = ++n
}

fun main() {
  //let() access object with it
  //returns last expression in lambda
  Tag(1).let {
    it.s = "let: ${it.n}
    it.increment()
  } eq 2
  //let with named lambda arg
  Tag(2).let { tag -> 
    tag.s = "let ${tag.n}
    tag.increment
  } eq 2
  //run() access object with this
  //returns last exp in lambda
  Tag(3).run {
    s = "run: $n" // implicit this
    increment() //implicit this
  } eq 4
  //with() access object with 'this'
  //returns last exp in lambda
  with(Tag(4)) {
    s = "with $n"
    increment()
  } eq 5
  // apply() access object with 'this'
  // returns modified obj
  Tag(5).apply {
    s = "apply: $n"
    increment
  } eq "Tag(n=6)"
  //also() access object with 'it' or named arg
  //returns modified obj
  Tag(6).also {
    it.s = "also ${it.n}"
    it.increment()
  } eq "Tag(n=7)"
}

Prefer run over with when the receiver is nullable and for call chains.

Otherwise you need to use a safe access operator before the scope function. The safe access operator to the context object null-checks the entire scope.

However, with requires you to null check even within scope.

When you use the safe access operator with let(), run() , apply(), or also() the entire scope is ignored for a null context object.

Example applying scope functions to a Map lookup:

package scopefunctions
import atomictest.*

data class Plumbus(var id: Int)

fun display(map: Map<String, Plumbus>) {
  trace("displaying $map")
  val pb1: Plumbus = map("main"]?.let {
    it.id _- 10
    it
  } ?: return
  trace(pb1)
  
  val pb2: Plumbus? = map["main"]?.run {
    id += 9
    this
  }
  trace(pb2)
  
  val pb3: Plumbus? = map["main"]?.apply {
    id += 8
  }
  trace(pb3)
  
  val pb4: Plumbus? = map["main"]?.also {
    it.id += 7
  }
  trace(pb4)
}

fun main() {
  display(mapOf("main" to Plumbus(1)))
  display(maoOf("none" to Plumbus(2)))
  
  trace eq """
    displaying {main=Plumbus(id=1)}
    Plumbus(id=11)
    Plumbus(id=20)
    Plumbus(id=28)
    Plumbus(id=35)
    displaying {none=Plumbus(id=2)}
  """
}

Example nesting scope functions where this can be ambiguous:

package scopefunctions
import atomictest.eq

fun nesting(s: String, i: Int): String =
  with(s) {
    with(i) {
      toString()
    }
  } +
  s.let {
    i.let {
      it.toString()
    }
  } +
  s.run {
    i.run {
      toString()
    }
  } +
  s.apply {
    i.apply {
      toString()
    }
  } +
  s.also {
    i.also {
      it.toString()
    }
  }
  
fun main() {
  nesting("X", 7) eq "777XX"
}

In all cases the call to toString() is appllied ot Int because the “closest” this or it is the Int implicit receiver.

apply() and also() return the modified obj s instead of the result of the calculation

None of the scope functions provide resource cleanup

use() does not allow anything to be returned from its lambda even though syntactically it is similar to let() and also().

To ensure cleanup use a scope function inside the use() lambda.

Scope functions are inlined

Creating Generics

Generic code works with types that are “specified later”

A single hierarchy used with concepts such as polymorphism can be limiting.

Sometimes an interface can be too restrictive because it forces you to work with o nly that interface.

Truly generalized code works if it works with some unspecified type.

That unspecified type is a generic type parameter

Any is the root of the Kotlin class hierarchy. Every Kotlin class has Any as a superclass.

One way to work with unspecified types is by passing Any arguments, and this can sometimes confuse the issue of when to use generics.

Two ways to use Any:

  1. You only need to operate on an Any and nothing more

    • There are only 3 direct member functions
    • There are other extension functions on Any, but they cannot perform direct operations on the type.
      • For examplek apply() only applies its function argument to the Any.
  2. If you know the type of the Any you can cast it and perform type specific operations. Because this requires run-time type information you risk a runtime error if you pass the wrong type to the function.

Example:

package creatinggenerics
import atomictest.eq

class Person {
  fun speak() = "Hi"
}

class Dog {
  fun bark() = "Ruff!"
}

class Robot {
  fun communicate() = "Beep!"
}

fun talk(speaker: Any) = when(speaker) {
  is Person -> speaker.speak()
  is Dog -> speaker.bark()
  is Robot -> speaker.communicate()
  else -> "Not a talker" // or exception
}

fun main() {
  talkPerson()) eq "Hi"
  talk(Dog()) eq "Ruff"
  talk(Robot()) eq "Beep!"
  talk(11) eq "Not a talker"
}

This is tolerable for a small project, but has maintainability problems

package creatinggenerics

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

class GClass<T>(val x: T) {
  fun f(): T = x
}

class GMemberFunction {
  fun <T> f(arg: T): T = arg
}

interface GInterface<T> {
  val x: T
  fun f(): T
}

class GImplementation<T>(
  override val x: T
): GInterface<T> {
  override fun f(): T = x
}

class ConcreteImplementation:
  GInterface<String> {
  override val x: String
    get() = "x"
  override fun f() = "f()"
}

fun basicGenerics() {
  gFunction("Yellow")
  gFunction(1)
  gFunction(Dog()).bark() // [1]
  gFunction<Dog>(Dog()).bark()
  
  GClass("Cyan").f()
  GClass(11).f()
  GClass(Dog()).f().bark() // [2]
  GClass<Dog>(Dog()).f().bark()
  
  GMemberFunction().f("Amber")
  GMemberFunction().f(111)
  GMemberFunction().f(Dog()).bark() // [3]
  GMemberFunction().f<Dog>(Dog()).bark()
  
  GImplementation("Cyan").f()
  GImplementation(11).f()
  GImplementation(Dog()).f().bark()
  
  ConcreteImplementation().f()
  ConcreteImplementation().x
}

[1], [2], and [3] are able to call bark() on the result because that result emerges to be type Dog

Ensure is the concept that within generic classes and functions can’t know the type T. Generics can be thought of as a way to preserve type information for the return value. This way you don’t have to write the code explicitly tocheck and cast a return value to a specific type.

Common use of generics is for containers:

package creatinggenerics
import atomictest.eq

class Car {
  override fun toString() = "Car
}

class CarCrate(private var c: Car) {
  fun put(car: Car) { c = car }
  fun get(): Car = c
}

fun main() {
  val cc = CarCrate(Car())
  val car: Car = cc.get()
  car eq "Car"
}

We can then make this more generic

package creatinggenerics
import atomictest.eq

open class Crate<T>(private var contents: T) {
  fun put(item: T) { contents = item }
  fun get(): T = contents
}

fun main() {
  val cc = Crate(Car())
  val car: Car = cc.get()
  car eq "Car"
  
}

Defining a generic extension function:

package creatinggenerics
import atomictest.eq

fun <T, R> Crate<T>.map(f: (T) -> R): List<R> =
  listOf(f(get()))

fun main() {
  Crate(Car()).map { it.toSTring() + "x" } eq
    "[Carx]"
}

A type parameter constraint says that the generic arg type must be inherited from the constraint.

Example without generics:

package creatinggenerics
import atomictest.eq

interface Disposable {
  val name: String
  fun action(): String
}

class Compost(override val name: String):
  Disposable {
   override fun action() = "Add to composter" 
}

interface Transport: Disposable

class Donation(override val name: String):
  Transport {
   override fun action() ="Call for pickup" 
}

class Recyclable(override val name: String):
  Transport {
   override fun action() = "Put in bin" 
}

class Landfill(override val name: String):
  Transport {
  override fun action() = "Put in dumpster" 
}

val items = listOf(
  Compost("Orange Peel"),
  Compost("Apple Core"),
  Donation("Couch"),
  Donation("Clothing"),
  Recyclable("Plastic"),
  Recyclable("Metal"),
  Recyclable("Cardboard"),
  Landfill("Trash")
)

val recyclables =
  items.filterIsInstance<Reclable>()

Using a constraint we can access properties and functions of the constrained type within a generic function:

package creatinggenerics
import atomictest.eq

fun <T: Disposable> nameOf(disposable: T) =
  disposable.name
  
//As an extension
fun <T: Disposable> T.name() = name

fun main() {
  recyclables.map { nameOF(it) } eq
    "{Plastic, Metal, Cardboard]"
  recyclables.map { it.name() } eq
    "[Plastic, Metal, Cardboard]"
}

We cannot access name without the constraint

This achieves the same result without generics:

package creatinggenerics
import atomictest.eq

fun nameOf2(disposable: Disposable) =
  disposable.name

fun Disposable.name2() = name

fun main() {
  recyclables.map { nameOf2(it) } eq
    "[Plastic, Metal, Cardboard]"
  recyclables.map { it.name2() } eq
    "[Plastic, Metal, Cardboard]"
}

Why use a constraint instead of ordinary polymorphism?

The answer is in the return type. With generics, the return type can be exact rather than being upcast to the base type:

package creatinggenerics
import kotlin.random.Random

private val rnd = Random(47)

fun List<Disposable>.aRandom(): Disposable =
  this[rnd.nextInt(size)]
  
fun <T: Disposable> List<T>.bRandom(): T =
  this[rnd.nextInt(size)]
  
fun <T> List<T>.cRandom(): T =
  this[rnd.nextInt(size)]
  
fun sameReturnType() {
  val a: Disposable = recyclables.aRandom()
  val b: Recyclable = recyclables.bRandom()
  val c: Recyclable = recyclables.cRandom()
}

Without generics, aRandom() can only produce Disposables.

bRandom() and cRandom() produce a Recyclable. bRandom() never accesses any elements of T, therefore its constraint is pointless and it ends up being hte same as cRandom().

The only time you need constraints is if:

  1. Access a function or property
  2. Preserve the type when returning it

In Java generic types are only available during compilation but are not preserved at runtime.

Example without type erasure:

package creating generics

fun main() {
  val strings = listOF("a", "b", "c')
  val all: List<Any> = listOf(1, 2, "x")
  useList(strings)
  useList(all)
}

fun useList(list: List<An>) {
  // if (this is List<String> {} // [1]
}

Uncommenting [1] produces error: “Cannot check for instance of erased type List

You can’t test for the generic type at runtime because the type information has been erased

Because generic types are erased, type information is not stored in the List. Instead both strings and all are just Lists with no additional type information

Type information is also erased for generic function calls, which means you can’t do much with a generic parameter inside a function.

To retian type information for function args add the reified keyword:

package creatinggenerics
import kotlin.reflect.KClass

fun <T: Any> a(kClass: KClass<T>) {
  //Use KClass<T>
}
package creatinggenerics

//Doesn't compiile because of erasure:
// fun <T: Any> b() = a(T::class)

The type information for T is erased when this cod eruns so b() won’t compile.

Java solution is to pass type information into the function by hand:

package creatinggenerics
import kotlin.reflect.KClass

fun <T: Any> c(kClass: KClass<T>) = a(kClass)

class K

val kc = c(K::class)

This is redundant because the compiler knows the type of T and could silently pass it for you:

package creatinggenerics

inline fun <reified T: Any> d() = a(T::class)

val kd = d<K>()

d() produces the same effect as c(), but d() doesn’t require the class reference as an arg.

Reification allows the use of is with a generic parameter type:

package creatinggenerics
import atomictest.eq

inline fun <reified T> check(T: Any) = t is T
// fun <T> check(t: Any) = t is T // [1]

fun main() {
  check<String>("1") eq true
  check<Int>("1") eq false
}

[1] Without reified the type information is erased so you can’t check whether a given element is an instance of T.

In the following example select() produces the name of each Disposable item of a particualr subtype. It uses reified combined with a constraint:

package creatinggenerics
import atomictest.eq

inline fun <reified T: Disposable> select() =
  items.filterIsInstance<T>().map { it.name }
  
fun main() {
  select<Compost>() eq
    "[Orange Peel, Apple Core]"
  select<Donation>() eq "[Couch, Clothing]"
  select<Recyclable>() eq
    "[Plastic, Metal, Cardboard]"
  select<Landfill>() eq "[Trash]"
}

filterIsInstance() is itself defined using the reified keyword

Combining generics and inheritance produces two dimensions of change. If you have a Container and want to assign it to a Container where T and U have an inheritance relationship, you must place constraints upon Container using the in or out variance annotations depending on how you want to use Container.

Example:

package variance

class Box<T>(private var contents: T) {
  fun put(item: T) { contents = item }
  fun get(): T = contents
}

class InBox<in T>(private var contents: T) {
  fun put(item: T) { contents = item }
}

class OutBox<out T>(private var contents: T) {
  fun get(): T = contents
}

in T means that member functions of the class can only accept args of type T, but cannot return values of type T.

out T means that member functions can return T objects, but cannot accept args of type T - you cannot place T objects into an OutBox.

Why do we need these constraints? Consider this hierarchy:

package variance

open class Pet
class Cat: Pet()
class Dog: Pet()

Cat and Dog are both subtypes of Pet. Is there a subtyping relation between Box and Box. It seems like we should be able to assign for example a Box of Cat to a Box of Pet or to a Box of Any (because Any is the supertype of everything):

package variance

val catBox = Box<Cat>(Cat())
// val petBox: Box<Pet> = catBox
// val anyBox: Box<Any> = catBox

If Kotlin allowed this, petBox would have put(item: Pet).Dog is also a Pet, so this would allow you to put a Dog into catBox, violating the “cat-ness” of that Box.

Worse, anyBox would have put(item: Any) so you could put Any into catBox which would result into no type safety at all.

The compiler allows us to assign an OutBox to an OutBox or to an OutBox, because the out annotation prevents them from having put() functions:

package variance

val outCatBox: OutBox<Cat> = OutBox(Cat())
val outPetBox: OutBox<Pet> = outCatBox
val outAnyBox: OutBox<Any> = outCatBox

fun getting() {
  val cat: Cat = outCatBox.get()
  val pet: Pet = outPetBOx.get()
  val any: Any = outAnyBox.get()
}

With no put(), we cannot place a Dog inside an OutBox, so its “cat-ness” is preserved.

Without a get(), an inBox can be assigned to an InBox, an InBox, or an InBox:

package variance

val inBoxAny: InBox<Any> = InBox(Any())
val inBoxPet: InBox<Pet> = inBoxAny
val inBoxCat: InBox<Cat> = inBoxAny
val inBoxDog: InBox<Dog> = inBoxAny

fun main() {
  inBoxAny.put(Any())
  inBoxAny.put(Pet())
  inBoxAny.put(Cat())
  inBOxAny.put(Dog())
  
  inBoxPet.put(Pet())
  inBoxPet.put(Cat())
  inBoxPet.put(Dog())
  
  inBoxCat.put(Cat())
  inBoxDog.put(Dog())
}

Box is invariant meaning that neither Box nor Box is a subtype of the other, so neither can be assigned to the other.
OutBox< out T> is covariant meaning that OutBox is a subtype of OutBox
InBox is contravariant meaning that InBox is a subtype of InBox. It varies in the opposite wa of upcasting Cat to Pet.

A read-only List from Kotlin is covariant

A MutableList is invariant

Functions can have covariant return types. This means that an overriding function can return a type that’s more specific than the function it overrides:

package variance

interface Parent
interface Child: Parent

interface X {
  fun f(): Parent
}

interface Y: X {
  override fun f(): Child
}

Operator Overloading

Operator overloading allows you to take an operator like + and give it meaning for your new type or exia meaning for an existing type.

Kotlin allows overloading of a limited set of operators

Example:

package operatoroverloading
import atomictest.eq

data class Num(val n: int)

operator fun Num.plus(rval: Num) =
  Num(n + rval.n)
  
fun main() {
  Num(4) + Num(5) eq Num(9)
  Num(4).plus(Num(5)) eq Num(9)
}

When you define an operator as a member function you can access private elements in a class that an extension function cannot.

Another example:

package operatoroverloading
import atomictest.eq

data class Molecule(
  val id: Int = idCount++,
  var attached: Molecule? = null
) {
  companion object {
    private var idCount = 0
  }
  operatur fun plus(other: Molecule) {
    attached = other
  }
}

fun main() {
  val m1 = Molecule()
  val m2 = Molecule()
  m1 + m2
  m1 eq "Molecule(id=0, attached=" +
    "Molecule(id=1, attached-null))"
}

This partially works (m2 + m1) would give you a stack overflow error

equals() compares ==

data classes come with a default equals() implementation, non data-classes compare refereneces by default

equals() is the only operator that cannot be an extension function, it must be overridden as a member function.

You are overriding equals(other: Any?)

Example:

package operatoroverloading
import atomictest.eq

class E(var v: Int) {
  override fun equals(other: Any?) =
    when {
      this === other -> true // [1]
      other !is E -> false // [2]
      else -> v == other.v // [3]
    }
    override fun hashCode(): Int = v
    override fun toString() = "E($v)"
}

fun main() {
  val a = E(1)
  val b = E(2)
  (a == b) eq false // a e.equals(b)
  (a != b) eq true // !a.equals(b)
  // Reference equality
  (E(1) === E(1)) eq false
}

[1] This is an optimization. If other refers to the same object in memory, the result is automatically true. The triple equality tests for reference equality.
[2] This determines that the type of other must be from the same as the urrent type.
[3] This compares the stored data. At this point the compiler knows that other is of type E.

When you override equals() you should also override hashCode()

When you compare nullable objects using ==, Kotlin enforces null -checking.

Example:

package operatoroverloading
import atomictest.eq

fun equalsWithIf(a: E?, b: E?) =
  if (a === null)
    b === null
  else
    a == b
    
fun equalsWithElvis(a: E?, b: E?) =
  a?.equals(b) ?: (b === null)
  
fun main() {
  val x: E? = null
  val y = E(0)
  val z: E? = null
  (x == y) eq false
  (x == z) eq true
  equalsWithIf(x, y) eq false
  equalsWithIf(x,z) eq true
  equalsWithElvis(x,y) eq false
  equalsWithElvis(x, z) eq true
}

Example defining arithmetic operators as extensions to class E:

package operator overloading
import atomictest.eq

//Unary Operators
operator fun E.unaryPlus() = E(v)
operator fun E.unaryMinus() = E(-v)
operator fun E.not() = this

 //Increment/decrement
 operator fun E.inc() = E(v + 1)
 operator fun E.dec() = E(v - 1)
 
fun unar(a: E) {
  +a
  -a
  !a
  
  var b = a
  b++
  b--   
}

//Binary operators
operator fun E.plus(e: E) = E(v + e.v)
operator fun E.minus(e: E) = E(v - e.v)
operator fun E.times(e: E) = E(v * e.v)
operator fun E.div(e: E) = E(v / e.v)
operator fun E.rem(e: E) = E(v % e.v)

fun binary(a: E, b: E) {
  a + b
  a - b
  a * b
  a / b
  a % b
}

// Augmented assignment
operator fun E.plusAssign(e: E) { v += e.v }
operator fun E.minusAssign(e: E) { v -= e.v }
operator fun E.timesAssign(e: E) { v *= e.v }
operator fun E.divAssign(e: E) { v /= e.v }
operator fun E.remAssign(e: E) { v %= e.v }

fun assignment(a: E, b: E) {
  a += b
  a -= b
  a *= b
  a /= b
  a %= b
}

fun main() {
  val two = E(2)
  val three = E(3)
  two + three eq E(5)
  two * three eq E(6)
  val thirteen = E(13)
  thirteen / three eq E(4)
  thirteen % three = E(1)
  val one = E(1)
  one += three * three
  one eq E(10)
}

x += e can be resolved to either x = x.plus(e) if x is a var or to x.plusAssign(e) if x is a val and the corresponding plusAssign() member is available. If both work the compiler emits an error indicating that it can’t choose

The parameter can be of a different type than the type the operator extends:

package operatoroverloading
import atomictest.eq

operator fun E.plus(i: Int) = E(v + i)

fun main() {
  E(1) + 10 eq E(11)
}

Operator precedence is fixed and is identical for built-in types and custom-types.

Binary plus has higher precedance than elvis operator. Use () to clarify order of operations

All comparison operators are automatically available when you define compareTo():

package operatoroverloading
import atomictest.eq

operator fun E.compareTo(e: E): Int =
  v.compareTo(e.v)
  
fun main() {
  val a = E(2)
  val b = E(3)
  (a < b) eq true // a.compareTo(b) < 0
  (a > b) eq false // a.compareTo(b) > 0
  //similarly for <= and >=
}

compareTo() must return an Int indicating:

  • 0 fi the elements are equal
  • pos value if the first value is bigger than the second value
  • neg value if the first element is smaller than the scond

rangeTo() overloads the .. operator for creating ranges while contains() indicates whether a value is within a range:

package operatoroverloading
import atomictest.eq

data class R(val r: IntRange) { // Range
  override fun toString() = "R($r)"
}

operator fun E.rangeTo(e: E) = R(v..e.v)
operator fun R.contains(e: E): Boolean =
  e.v in r
  
fun main() {
  val a = E(2)
  val b = E(3)
  val r = a..b // a.rangeTo(b)
  (a in r) eq true // r.contains(a)
  (a !in r) eq false // !r.contains(a)
  r eq $(2..3)
}

overloading contains() allows you to check whether a value is in a container while get() and set() support reading and assigning elements in a container using square brackets:

package operatoroverloading
import atomictest.eq

data class C(val c: MutableList<Int>) {
  override fun toString() = "C($c)"
}

operator fun C.contains(e: E) = e.v in c

operator fun C.get(i: Int): E = E[c[i])

operator fun C.set(i: Int, e: E) {
  c[i] = e.v
}

fun main() {
  val c = C(mutableLIstOf(2, 3))
  (E(2) in c) eq true // c.contains(E(2))
  c[1] eq E(3) // c.get(1)
  c[1] = E(4) // c.set(2, E(4))
  c eq C(mutablelist(2, 4))
}

In IntellliJ or Android Studio you can use navigate to a declaration of a function or class from its usage. This also works with operators: you can put the cursor on .. then navigate to its definition to see which operator function is called.

Placing () after an object calls invoke():

package operatoroverloading
import atomictest.eq

class Func {
  operator fun invoke() = "invoke()"
  operator fun invoke(i: Int) = "invoke($i)"
  operator fun invoke(i: Int, j: String) =
    "invoke($i, $j"
  operator fun invoke(
    i: Int, j: String, k: Double
  ) = "invoke($i, $j, $k)"
}

fun main() {
  val f = Func()
  f() eq "invoke()"
  f(22) eq "invoke(22)"
  f(22, "Hi") eq "invoke(22, Hi)"
  f(22, "Three", 3.14156) eq
    "invoke(22, Three, 3.1416)"
}

You can also define invoke() with vararg.

invoke() can also be an extension function.

If you have a function reference you can use it to call the function directly using () or via invoke():

package operatoroverloading
import atomictest.eq

fun main() {
  val func: (String) -> Int = { it.length }
  func("ab") eq 3
  func.invoke("abc") eq 3
  
  val nullableFunc: ((String -> Int)? = null
  if (nullableFunc != null) {
    nullableFunc("abc")
  }
  nullableFunc?.invoke("abc") // [1]
}

If a func reference is nullable you can combine invoke() and safe access.

The most common use for a custom invoke is creating a DSL

Using Operators

Usually you only overload operators when creating your own library

You get more benefits when implementing Comparable interface and overriding its compareTo:

package usingoperatiors
import atomictest.eq

data class Contact(
  val name: String,
  val mobile: String
): Comparable<Contact> {
  override fun compareTO(
    other: Contact
  ): Int = name.compareTo(other.name)
}

fun main() {
  val alice = Contact("Alice", "01234567890")
  val bob = Contact("Bob", "9876543210")
  val carl = Contact("Carl", "5678901234")
  (alice < bob) eq true
  (alice <= bob) eq true
  (alice > bob) eq false
  (alice >= bob) eq false
  val contacts = listOf(bob, carl, alice)
  contacts.sorted() eq
    listOf(alice, bob, caarl)
  contacts.sortedDescending() eq
    listOf(carl, bob, alice)
}

Comparable does not include comparison by == and !=

Kotlin doesn’t require the operator modifier when overriding compareTo() because it has already been defined as an operator in the Comaprable interface.

Implementing Comparable also enables features like sortability and creating a range of instances without redefining the .. operator. You can then check to see if a value is in that range:

package usingoperators
import atomictest.eq

class F(val i: Int): Comparable<F> {
  override fun compareTo(other: F) =
    i.compareTo(other.i)
}

fun main() {
  val range = F(1)..F(7)
  (F(3) in range) eq true
  (F(9) in range) eq false
}

Prefer implementing Comparable to compareTo.

You will have to use compareTo as an extension when working with a class you have no control over.

Example defining componentN() functions for destructuring declarations. Kotlin can then quietly call these during destructuring assignment:

package usingoperators
import atomictest.*

class DUo(val x: Int, val y: Int) {
  operator fun component1(): Int {
    trace("component1()")
    return x
  }
  operator fun component2(): Int {
    trace("component2()")
    return y
  }
}

fun main() {
  val (a, b) = Duo(1, 2)
  a eq 1
  b eq 2
  trace eq "component1() component2()"
}

data classes automatically create componentN() functions

package usingoperators
import atomictest.eq

data class Person(
  val name: String
  val age: Int
) {
   //Compiler generates
   // fun component1() = name
   // fun component2() = age
}

fun main() {
  val person = Person("Alice", 29)
  val (name, age) = person
  // Destructuring assignment becomes
  val name_ = person.component1()
  val age_ = person.component2()
  name eq "Alce"
  name_ eq "Alice"
  age eq 29
  age_ eq 29
}

Property Delegation

A property can delegate its accessor logic

The delegate’s class must contain a getValue() function if the property is a val or getValue() and setValue() functions if the value is a var

Example read only case:

package propertydelegation
import atomictest.eq
import kotlin.reflect.KProperty

class Readable(val i: Int) {
  val value: String by BasicRead()
}

class BasicRead {
  operator fun getValue(
    r: Readable
    property: KProperty<*>
  ) = "getValue: ${r.i}"
}

fun main() {
  val x = Readable(11)
  val y = Readable(17)
  x.value eq "getValue: 11"
  y.value eq "getValue: 17"
}

value in Readable is delgated to a BasicRead object. getValue() takes a Readable parameter that allows it to access the Readable - when you so it binds the BasicRead object to the whole Readable object.

Notice that getValue() accesses i in Readable

Because getValue() returns a String the type of value must also be String

The second getValue() parameter property is of the special type KProperty and this provides reflective information about the delegated property

If the delegated property is a var you must handle reading and writing:

package propertydelegation
import atomictest.eq
import kotlin.reflect.KProperty

class ReadWritable(var i: Int) {
  var msg = ""
  var value: String by BasicReadWrite()
}

class BasicReadWrite {
  operator fun getValue(
    rw: ReadWritable,
    property: KProperty<*>
  ) = "getValue: ${rw.i}"
  operator fun setValue(
    rw: ReadWritable
    property: KProperty<*>,
    s: String
  ) {
    rw.i = s.toIntOrNull() ?: 0
    rw.msg = "setValue to ${rw.i}"
  }
}

fun main() {
  val x = ReadWritable(11)
  x.value eq "getValue: 11"
  x.value = "99"
  x.msg eq "setValue to 99"
  x.value eq "getValue: 99"
}

getValue and setValue() must agree on the type that is read and written

A class can be used as a delegate if it simply conforms to the convention of having the necessary function(s) with the necessary signature(s). However, you can also implement ReadOnlyProperty interface as seen here:

package propertyDelegation
import atomictest.eq
import kotlin.properties.ReadOnllyProperty
import kotlin.reflect.KProperty

class Readable2(val i: Int) {
  val value: String by BasicRead2()
  //SAM Conversion
  val value2: String by 
    ReadOnlyProperty { _, _ -> "getValue: $i" }
}

class BasicRead2: ReadOnly Property<Readable2, String> {
  override operator fun getValue(
    thisRef: Readable2,
    property: KProperty<*>
  ) = "getValue: ${thisRef.i}"
}

fun main() {
  val x = Readable2(11)
  val y = Readable2(17)
  x.value eq "getValue: 11"
  x.value2 eq "getValue: 11"
  y.value eq "getValue:: 17"
  y.value2 eq "getValue: 17"
}

Implementing ReadOnlyProperty communicates that BasicRead2 can be used as a delegate and ensures a proper getValue() definition.

Because ReadOnlyProperty has only a single member function (and it has been defined as a fun interface int he standard library, value2 is defined using a SAM conversion

Modified example to implement ReadWriteProperty

package propertydelegation
import atomictest.eq
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class ReadWriteable2(var i: Int) {
  var msg = ""
  var value: String by BasicReadWrite2()
}

class BasicReadWrite2:
  ReadWriteProperty<ReadWriteable2, String>{
  override operator fun getValue(
    rw: ReadWriteable2,
    property: KProperty<*>
  ) = "getValue: ${rw.i}"
  
  override operator fun setValue(
    rw: ReadWriteable2,
    property: KProperty<*>,
    s: String
  ) {
    rw.i = s.toIntOrNull() ?: 0
    rw.msg = "setValue to ${rw.i}"
  }
}

fun main() {
  val x = ReadWriteable2(11)
  x.value eq "getValue: 11"
  x.value = "99"
  x.msg eq "setValue to 99"
  x.value = "getValue: 99"
}

Thus, a delegate class must contain either or both of the following functions which are called when the delegated property is accessed:

  1. For reading getValue()
  2. For Writing setValue()

If the delegated property is a val, only the first function is required and ReadOnlyProperty can be implemented using a SAM conversion.

The parameters are:

  • thisRef: T points to the delegate object, where T is the type of that delegate. IF you do not want to use thisRef in the function you can disable it using Any? for T.
  • property: KProperty<*> provides information about the property itself. The most commonly-used is name which produces the field name of the delegated property.
  • value: is the value stored by setValue() into the delegated propert. V is the type of that property.

To enable access to private elements nest the delegate class.

Assuming appropriate access, getValue(0 and setValue() can be written as extension functions. This way you can still sue an existing class and still delegate a property with it.

A local delegated property is defined inside a function rather than a class

Property Delegation Tools

The standard library contains special property delegation operations.

Map is one of the few types in the Kotlin library that is preconfigured to be used as a delegated property. A single Map can be used to store all the properties in a class. Each property identifier becoems a String key for the map, and the property’s type is captured in the associated value:

package propertydelegation
import atomictest.eq

class Driver(
  map: MutableMap<String, Any?>
) {
  var name: String by map
  var age: Int by map
  var id: String by map
  var available: Boolean by map
  var coord: pair<Double, Double> by map
}

fun main() {
  val info = mutableMapOf<String, Any?>(
    "name" to "Bruno Fiat",
    "age" to 22,
    "id" to "123456",
    "available" to false,
    "coord" to Pair(111.93, 1231.12)
  )
  val driver = Driver(info)
  driver.available eq false
  driver.available = true
  info eq "{name=Bruno Fiat, age=22, " +
    "id=123456, available=true, " +
    "coord=(111.93, 1231.12)" +
    "}"
}

The original Map info is modified when the setting driver.available = true

Kotlin standard library contains Map extension functions getValue() and setValue() that enables property delegation.

Delegates.observable() observes modifications of a mutable property. Here, we trace old and new values:

package delegationtools
import kotlin.properties.Delegates.observable
import atomictest.eq

class Team {
  var msg = ""
  var captain: String by observable("<0>") {
    prop, old, new ->
      msg = "${prop.name} $old to $new "
  }
}

fun main() {
  val team = Team()
  team.captain = "Adam"
  team.captain = "Amanda"
  team.msg eq "captain <0> to Adam " +
    "captain Adam to Amanda"
}

observable’s two args:

  • <0> is the initial value for the property
  • a function which is the action to perform when the propert is modified. The function args are the property being changed, the current value, and the new value.

Delegates.vetoable() allows you to prevent a change to a property if the new property value doesn’t satisfy the given predicate:

package delegatetools
import atomictest.*
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

fun aName(
  property: KProperty<*>,
  old: String,
  new: String
) = if (new.startsWith("A")) {
  trace("$old -> $new")
  true
} else {
  trace("Name must start with 'A'")
  false
}

interface Captain {
  var captain: String
}

class TeamWithTraditions: Captain {
  override var captain: String by
    Delegates.vetoable("Adam", ::aName)
}

class TeamWithTraditions2: Captain {
  override var captain: String by
    Delegates.vetoable("Adam") {
      _, old, new ->
        if (new.startsWith("A") {
          trace("$old -> $new")
          true
        } else {
          trace("Name must start with 'A'")
          false
        }
    }
}

fun main() {
  listOf(
    TeamWithTraditions(),
    TeamWithTraditions2()
  ).forEach {
    it.captain = "Amanda"
    it.captain = "Bill"
    it.captain eq "Amanda"
  }
  trace eq """
    Adam -> Amanda
    Name must start with 'A'
    Adam -> Amanda
    Name must start with 'A'
  """
}

Delegates.vetoable() takes two args

  1. The initial value
  2. an onChange() function which itself takes three args
    1. property
    2. current (old) value
    3. new value
      • The function returns a Boolean indicating whether the change was successful.

properties.Delegates.notNull() produces a property that must be initialized before it is read:

package delegationtools
import atomictest.*
import kotlin.properties.Delegates

class NeverNull {
  var nn: Int by Delegates.notNull()
}

fun main() {
  val non = NeverNull()
  capture {
    non.nn
  } eq "IllegalStateException: Property " +
    "nn should be initialized before get."
  non.nn = 11
  non.nn eq 11
}

Lazy Initialization

Use a lazy property when initialization is costly

lazy property is initialized when it’s first used rather than when it’s created.

val lazyProperty by lazy { initializer }

lazy() takes a lambda containing the init logic. The last expression in the lambda becomes the result and is assigned to the property:

package lazyinitialization
import atomictest.*

val idle: String by lazy {
  trace("Initializing 'idle'")
  "I'm never used"
}
val helpful: String by lazy {
  trace("Initializing 'helpful'")
  "I'm helpful"
}

fun main() {
  trace(helpful)
  trace eq """
    Initializing 'helpful'
    I'm helpful
  """
}

Above the idle property is never initialized because it is never accessed.

Above helpful and idle are vals. Without lazy init you’d be forced to make them vars producing less reliable code.

Late Initialization

Sometimes you want to init properties of your class after it is created, but in a separate member function instead of using lazy

For example, a framework or library might require initialization in a special function. If you extend that library class, you can provide your own implementation of that special function.

Example:

package lateinitialization
import atomictest.eq

interface Bag {
  fun setUp()
}

class Suitcase: Bag {
  private var items: String? = null
  override fun setUp() {
    items = "socks, jacket, laptop"
  }
  fun checkSocks(): Boolean =
    items?.contains("socks") ?: false
}

fun main() {
  val suitcase = Suitcase()
  suitcase.setUp()
  suitcase.checkSocks() eq true
}

Suitcase initializes items by overriding setUp()

We can’t define items as a String because if we did that we must provide a non-null init in the constructor.

Using a stub value such as an empty String is bad practice because you never know whether it’s actually been initialized. null indicates that it’s not initiallized.

Defining items as String? means we must check for null in all member functions as in checkSocks().

However, we know that the library we’re reusing initializes items by calling setUp() so the null checks are not neccessary. This problem is fixed by using lateinit:

package lateinitialization
import atomictest.eq

class BetterSuitcase: Bag {
  lateinit var items: String
  override fun setUp() {
    items = "socks, jacket, laptop"
  }
  fun checkSocks() = "socks" in items
}

fun main() {
  val suitcase = BetterSuitcase()
  suitcase.setUp()
  suitcase.checkSocks() eq true
}

Above we define items as a non-nullable property

lateinit can be used on a property inside the body of a class, a top-level property, or local var

Limitations:

  • lateinit can onlly be used on a var property not a val
  • The property must be non-nullable type
  • The property cannot be a primitive type
  • lateinit is not allowed for abstract properties in an abstract class or interface
  • lateinit is not allowed for properties with a custom get() or set()

isInitialized tells you whether a lateinit property has been initialized. The property must be in our current scope and is accessed using the :: operator.

package lateinitialization
import atomictest.*

class WithLate {
  lateinit var x: String
  fun status() = "${""xisinitialized}"
}

lateinit var y: String

fun main() {
  trace("${::y.isInitialized}")
  y = "Ready"
  trace("${y::y.isInitialized}")
  val withlate = WithLate()
  trace(withlate.status())
  withlate.x = "Set"
  trace(withlate.status())
  trace eq """
    false
    true
    false
    true
  """
}

Although you can create a local lateinit var, you cannot call isInitialized on it because references to local vars is not supported.