56 KiB
Official list of idioms: https://kotlinlang.org/docs/idioms.html
Original repository: https://code.mipt.ru/SPC/education/ks-materials
Preamble/Important notes¶
- "Syntactic sugar" is a false concept. Each language feature affects how we use the language.
- One can adopt different programming styles in Kotlin. Idiomatic kotlin is an unofficial combination of practices developed by experienced Kotlin users.
- One of the key features of idiomatic kotlin is to use scopes instead of objects to provide incapsulation and limit visible mutable state.
What is idiomatic Kotlin (roughly)?¶
- Less "ceremony". Pragmatic approach.
- Hybrid object/functional paradigm (objects for data, functions for interactions).
- Don't use mutable state wherever read-only is enough.
- Don't use classes when functions are enough.
- Don't use inheritance when composition is enough.
- Function is a first-class citizen.
- Scope is a first-class citizen.
- Use extensions to separate inner workings from utilities.
String interpolation¶
Interpolated string allows constructing string fast
println("This is a plain string")
val arg = "Interpolated" println("This is an $arg string")
val number = 1 fun increment(num: Int) = num + 1 println("This is an $arg string number ${increment(number) + 1}")
println("This is a string with \$")
println(""" This is a multi line raw string """.trimIndent())
println("""This is a raw string number ${number+1} with \ and ${'$'} """)
Make val
, not var
!¶
val
means read-only property
var
means writable property
Unnecessary use of var
is not recommended.
NOTE: In performance critical cases, explicit cycles are better, but mutable state should be encapsulated.
/* Not recommended */ var counter = 0 for(i in 0..20){ counter += i } counter
/* recommended */ val sum = (0..20).sum() sum
val intArray: IntArray = IntArray(21){ it } intArray.sum()
Top level functions¶
Don't create a class when a function is enough.
Use access modifiers for incapsulation.
/** * Functions could be defined everywhere in kotlin code. This is a so-called top-level function. */ fun doSomething(){ println("I did it") } doSomething()
object AnObject{ /** * This is a member-function */ fun doSomethingInAnObject(){ println("I did it in an object") } } AnObject.doSomethingInAnObject()
fun doSomethingSpecial(){ val special = "special" /** * This one is inside another function */ fun doSomethingInside(){ println("I did $special inside another function") } doSomethingInside() } doSomethingSpecial()
fun returnFunction(): (String) -> Unit{ fun printStringWithPrefix(str: String){ println("Prefixed: $str") } return ::printStringWithPrefix // return { printStringWithPrefix(it) } // return { println("Prefixed: $it") } } returnFunction()("Haskel, Boo!")
Unit¶
fun returnUnit(): Unit{ // no return statement `Unit` is returned implicitly val b = Unit }
Default parameters¶
fun doSomethingWithDefault(a: Int = 0, b: String = "") { println("A string with a == $a and b == \"$b\"") } doSomethingWithDefault() doSomethingWithDefault(2, "aaa") doSomethingWithDefault(b = "fff", a = 8) doSomethingWithDefault(4, b = "fff") doSomethingWithDefault(a = 2, "fff")// don't do that
fun integrate( from: Double = 0.0, to: Double = 1.0, step: Double = 0.01, function: (Double) -> Double, ): Double { require(to > from) require(step > 0) var sum = 0.0 var pos = from while (pos < to) { sum += function(pos) pos += step } return sum*step } integrate { x -> x * x + 2 * x + 1 } integrate(0.0, PI) { sin(it) } integrate(0.0, step = 0.02, to = PI) { sin(it) } //integrate(0.0, step = 0.02, PI) { sin(it) }
Function body shortcut¶
fun theSameAsBefore(a: Int = 0, b: String = ""): String = "A string with a == $a and b == $b" class Context(val a: String, val b: String) fun somethingWith(context: Context) = with(context){ println(a+b) }
Trailing lambda¶
Scope is a separate important concept in Kotlin
data class BuildResult(val str: String, val int: Int) class Builder(var str: String = "", var int: Int = 0){ fun build() = BuildResult(str, int) } fun doWithBuilder(defaultInt: Int, block: Builder.() -> Unit): BuildResult = Builder(int = defaultInt).apply(block).build() doWithBuilder(2){ str = "ff" }
Inheritance¶
//Interfaces and objects interface AnInterface { val a: Int get() = 4 // set(value){ // println(value) // } fun doSomething() //= println("From interface") } abstract class AnAbstractClass(override val a: Int): AnInterface{ override final fun doSomething() = println("From a class") abstract fun doSomethingElse() } class AClass(override val a: Int) : AnInterface { override fun doSomething() = println("From a class") } class BClass(a: Int) : AnAbstractClass(a) { override fun doSomethingElse() = println("something else") } /** * A singleton (object) is a type (class) which have only one instance */ object AnObject : AnInterface { override fun doSomething(): Unit = TODO("Not yet implemented") } /** * Creating an instance */ val instance = AClass(3) /** * Using singleton reference without constructor invocation */ val obj: AnInterface = AnObject /** * Anonymous object */ val anonymous = object : AnInterface { override fun doSomething(): Unit = TODO("Not yet implemented") } /** * The one that should not be named */ val voldemort = object { fun doSomething(): Unit = TODO() } //voldemort.doSomething()
Declaration site variance¶
In consumes, out produces.
interface Producer<out T>{ fun produce(): T } interface Consumer<in T>{ fun consume(value: T) }
fun transform(color: String): Int = when (color) { "Red" -> 0 "Green" -> 1 "Blue" -> 2 else -> throw IllegalArgumentException("Invalid color param value") }
Runtime type dispatch¶
In Java it is called DOP (Data Oriented Programming).
Also sometimes (wrongly) called patter matching.
/** * Matching by type */ fun checkType(arg: Any): Unit = when(arg){ is String -> println("I am a String") is Int -> println("I am an Int") is Double -> println("I am a Double") // 2==2 -> println("Wat?") else -> println("I don't know who am I?") } fun checkType2(arg: Any): Unit = when{ arg is String -> println("I am a String") arg is Int -> println("I am an Int") arg is Double -> println("I am a Double") 2==2 -> println("Wat?") else -> println("I don't know who am I?") } checkType(true)
Try-catch expression¶
https://kotlinlang.org/docs/idioms.html#try-catch-expression
fun tryCatch() { val result = try { count() } catch (e: ArithmeticException) { throw IllegalStateException(e) } // Working with result }
Data class¶
class SimpleClass(val a: Int, val b: Double, c: Double){ override fun toString() = "SimpleClass(a=$a, b=$b)" } data class DataClass(val a: Int, val b: Double) { val c get() = b + 1 } val simple = SimpleClass(1, 2.0, 3.0) println(simple) val data = DataClass(2, 2.0) val copy = data.copy(b = 22.2) println(copy)
Nothing¶
/** * The declaration if valid because [TODO] returns Nothing */ fun getSomething(): Int? = TODO() fun findSomething(): Int { // early return is valid because `return` operator result is Nothing val found: Int = getSomething() ?: return 2 return found + 2 } fun checkCondition(): Int { fun conditionSatisfied() = false return if (conditionSatisfied()) { 1 } else { //error is Nothing error("Condition is not satisfied") } }
Nullable truth¶
/** * idiom 10 * Nullable truth. */ fun idiom10() { class A(val b: Boolean?) val a: A? = A(null) // Compilator warning here //use a?.b == true //instead of a?.b ?: false // The old way val res = if (a == null) { false } else { if (a.b == null) { false } else { a.b } } }
Safe call¶
//TODO move up /** * Safe call and elvis operator */ fun idiom12() { val files = File("Test").listFiles() println(files?.size ?: "empty") } idiom12()
Nullable assignment¶
/** * Dart-like (?=) nullable assignment */ fun idiom11() { var x: String? = null x = x ?: "Hello" }
Safe calls¶
fun printNotNull(any: Any) = println(any) // printNotNull(null) does not work val value: Int? = 2 //val value: Int? by lazy{ 2 } //printNotNull(value) // Error if (value != null) { //not guaranteed to work with mutable variable printNotNull(value) } var value1: Int? = 2 // Safe call here value1?.let { printNotNull(it) // execute this block if not null //printNotNull(value1) // value is not null here }
Extension functions¶
/** * Extension function on a String. It is not monkey-patching. */ fun String.countOs(): Int = count { it == 'о' } // implicit this points to String fun Int.printMe() = println(this) // explicit this fun Any?.doSomething() = TODO("Don't do that") "вылысыпыдыстычка".countOs().printMe()
infix fun <T: Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): ClosedRange<T>?{ val start = maxOf(this.start, other.start) val end = minOf(this.endInclusive, other.endInclusive) return if(end>=start) start..end else null } (0..8).intersect(6..12) 0..8 intersect 6..12
Extension properties¶
Remember property usage guidelines.
/** * Extension property (must be virtual) */ val List<Int>.odd get() = filter { it % 2 == 1 } List(10){it}.odd
/** * Extension variable (also must be virtual) */ var Array<Int>.second: Int get() = this[1] set(value) { this[1] = value } val array = Array(5){it} array.second = 9 array
Scope functions (run, with, let)¶
object AClass{ val a = "a" val b = "b" val c = "c" } fun getAClass(): AClass? = null /** * [with]/[run]/[let] are used to create a scope with given argument or receiver */ // Simple print of AClass println("a = ${AClass.a}, b = ${AClass.b}, c = ${AClass.c}") // Using `with` val res = with(AClass){ // AClass type is the receiver in this scope println("a = $a, b = $b, c = $c") /*return@with*/ "some value" } res
//using `run` getAClass()?.run { // the same as above println("a = $a, b= $b, c = $c") } //Using `let` to compose result. Not recommended using without a need val letResult = getAClass()?.let { arg -> arg.c + arg.a }
Scope functions (also, apply)¶
/** * Using apply/also to add a finalizing action */ var i = 2 /** * [also] block does not affect the result */ fun getAndIncrement() = i.also { i += 1 } println(getAndIncrement()) println(i)
class Rectangle{ var length: Number = 0 var breadth: Number = 0 var color: Int = 0xffffff } /** * Configure properties of an object (apply) * https://kotlinlang.org/docs/idioms.html#configure-properties-of-an-object-apply */ val myRectangle = Rectangle().apply { length = 4 breadth = 5 color = 0xFAFAFA }
Lists¶
Do not cast List to MutableList!
/** * Lists and mutable lists */ /** * This method creates a read-only list of strings. One should note that the type of the list is not specified */ val list = listOf("a", "b", "c") println(list[0]) println(list.get(1)) //println(list.get(4)) println(list.getOrNull(4) ?: "nothing")
/** * This one creates a mutable list */ val mutableList: MutableList<String> = mutableListOf("a", "b", "c") mutableList[2] = "d" mutableList.add("e") mutableList += "f" mutableList
/** * This one creates a mutable ArrayList. */ val arrayList: ArrayList<String> = arrayListOf("a", "b", "c") //Danger zone val newList: List<String> = list + "f" + mutableList println(newList)
//Bonus val lambdaList = List(3){ it.toString() } println(lambdaList) val builderList: List<Int> = buildList{ add(2) add(8) remove(8) } builderList
// val sequentialList = List(20){it} // sequentialList.zipWithNext{ l, r -> r - l }.forEach{println(it)}
Shortcut collection builders¶
/** * Use shortcut function to provide default values */ fun doSomething(additionalArguments: List<String> = emptyList()){ TODO() emptyArray<String>() emptySet<String>() emptyMap<String,String>() } emptyList<String>()::class
Maps¶
val map = mutableMapOf( "key" to "a", "key2" to "b", ) //The map could be accessed via indexing operation println(map["key"]) map["key"] = "fff"
//val entry: MutableMap.MutableEntry<String, String> = map.iterator().next() /** * The destructuring declaration for maps and other objects that support `operator fun componentN` */ for ((k, v) in map) { //val (k, v) = entry // val k = entry.component1() // val v = entry.component2() println("$k -> $v") }
map.forEach { (k, v) -> println("$k -> $v")}
val (a, b) = Pair(1, 2) val coord = doubleArrayOf(0.0, 1.0, 2.0) val (x,y,z) = coord data class Coordinate(val x: Double, val y: Int) val (x1, y1) = Coordinate(1.0,2)
Mutable type access decorator¶
To be replaced with qualified getter types soon
class AClassWithList{ var b: Double = 2.0 private set init{ b = 5.0 } private val _list: MutableList<Int> = ArrayList<Int>() val list: List<Int> get() = _list } val obj = AClassWithList() // obj.b = 10.0 //error obj.list
Contains operator and ranges¶
val emailsList = emptyList<String>() // When used directly infix in operator checks if the element is contained in a collection //using `operator fun contains` if ("john@example.com" in emailsList) { println("is in list") } if ("jane@example.com" !in emailsList) { println("not in list") }
class DateRange(val start: Instant, val end: Instant) operator fun DateRange.contains(value: Instant): Boolean = value > start && value < end println(Instant.now() in DateRange(Instant.EPOCH, Instant.MAX))
// Another (different) use of `in` is iteration over range or collection using // using `operator fun iterator` for (i in 1..100) { println(i) } // closed range: includes 100 (1..100).forEach { i -> println(i) } //the same, but with boxing for (i in 1..<100) { println(i) } // half-open range: does not include 100 for (x in 2..10 step 2) { println(x) } for (x in 10 downTo 1) { println(x) } infix fun ClosedRange<Double>.step(step: Double): Sequence<Double> { //TODO check arguments var current = start return sequence { do { yield(current) current += step } while (current <= endInclusive) } } for (x in 0.0..10.0 step 0.5){ println(x) }
Map-reduce¶
val list: List<Int> = listOf(1, 2, 3, 4, 5, 6) val result = list //.stream().parallel() //.asSequence() .filter { it % 2 == 0 } //select even numbers .map { it * it } // get square of each element //.onEach { println(it) } //.sumOf { it } //use one of reduce operations .reduce { acc: Int, i: Int -> acc + i } result
val sequence = sequence{ var counter = 1 while(true){ yield(counter++) // println(counter) } } val result = sequence .take(6) .filter { it % 2 == 0 } //select even numbers .map { it * it } // get square of each element //.onEach { println(it) } //.sumOf { it } //use one of reduce operations .reduce { acc: Int, i: Int -> acc + i } result
Wrap mutable logic¶
val list = buildList { repeat(10){ add(it) } } println(list)
Scoped resource usage¶
val stream = Files.newInputStream(Paths.get("file.txt")) // The resource is automatically closed when leaving the scope stream.bufferedReader().use { reader -> println(reader.readText()) }
Factory as parameter and companion factories¶
interface Factory<T : Any> { fun build(str: String): T } data class IntContainer(val arg: Int) { companion object : Factory<IntContainer> { override fun build(str: String) = IntContainer(str.toInt()) } } data class DoubleContainer(val arg: Double) { companion object : Factory<DoubleContainer> { override fun build(str: String) = DoubleContainer(str.toDouble()) } } fun <T : Any> buildContainer(str: String, factory: Factory<T>): T = factory.build(str) buildContainer("22", IntContainer)
Initialization¶
open class Bad{ val value: Int init { value = requestValue() } open fun requestValue(): Int { doSomethingElse() return 2 } private fun doSomethingElse(){ println(value) } } Bad()
//Factory functions are preferred to the initialization logic data class Good internal constructor(val value: Int){ init { //Initialization block is there to check arguments require(value >= 0) } companion object } fun requestValue(): Int = TODO() // This is the factory-function fun Good() = Good(requestValue()) // additional constructor-like builders could be added to the companion @OptIn(ExperimentalUnsignedTypes::class) fun Good.Companion.build(value: UInt) = Good(value.toInt()) Good.build(32U)
Delegates¶
class ClassWithALazyProperty{ //Use lazy delegate for something that should be calculated ones on first call val lazyValue by lazy { //Do dome heavy logic here println("Initialized") 22 } val getterValue: Int get(){ println("got") return 33 } } val lazyClass = ClassWithALazyProperty() lazyClass.lazyValue lazyClass.lazyValue lazyClass.getterValue lazyClass.getterValue
//Using other delegates val map = mapOf("a" to 1, "b" to 2) val a: Int by map println(a)
Inline functions¶
/** * Definition of inline function */ inline fun List<Int>.forEachOdd(block: (Int) -> Unit) = forEach { if (it % 2 == 1) block(it) } /** * The demonstration of use of inline [forEach] function with non-local return */ fun foo() { listOf(1, 2, 3, 4, 5).forEach { if (it == 3) return // non-local return directly to the caller of foo() print("$it, ") } println("this point is unreachable") } foo()
/** * Using inline function for type reification during the compile time */ inline fun <reified T> List<T>.prettyPrint() = forEach { when (T::class) { Double::class -> println("Double: ${it as Double}") Int::class -> println("Int: ${it as Int}") else -> it.toString() } } inline fun <reified T> prettyPrintOne(arg: T) = listOf(arg).prettyPrint() /** * **WARNING** inline functions are an advanced feature and should be used only for * reification or non-local return * NOT for optimization. */
Collections and boxing¶
/** * Values are boxed. Each call is indirect */ val list: List<Double> = List(20) { it.toDouble() } /** * Still boxed */ val genericArray: Array<Double> = Array(20) { it.toDouble() } /** * Non-boxed */ val specializedArray: DoubleArray = DoubleArray(20) { it.toDouble() }