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:
- It creates objects in a multi-step process
- It produces different object variations using the same basic code
- It separates common code from specialized code
- Enables creation of a DSL
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:
- let
- run
- with
- apply
- 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:
-
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.
-
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:
- Access a function or property
- 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
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
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
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
Without a get(), 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
OutBox< out T> is covariant meaning that OutBox
InBox
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:
- For reading getValue()
- 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
- The initial value
- an onChange() function which itself takes three args
- property
- current (old) value
- 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.