66 KiB
66 KiB
- Official list of idioms: https://kotlinlang.org/docs/idioms.html
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.
Do not do¶
- Scope pollution.
- Scope function abuse.
- Rely on initialization order.
String interpolation¶
Interpolated string allows constructing string fast
In [ ]:
println("This is a plain string")
In [ ]:
val arg = "Interpolated" println("This is an $arg string")
In [ ]:
val number = 1 fun increment(num: Int) = num + 1 println("This is an $arg string number ${increment(number) + 1}")
In [ ]:
println("This is a string with \$")
In [ ]:
println(""" This is a multi line raw string """.trimIndent())
In [ ]:
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.
In [ ]:
/* Not recommended */ var counter = 0 for(i in 0..20){ counter += i } counter
In [ ]:
/* recommended */ val sum = (0..20).sum() sum
In [ ]:
val intArray: IntArray = IntArray(21){ it -> it + 1 } intArray.sum()
Top level functions¶
Don't create a class when a function is enough.
Use access modifiers for incapsulation.
In [ ]:
/** * Functions could be defined everywhere in kotlin code. This is a so-called top-level function. */ fun doSomething(){ println("I did it") } doSomething()
In [ ]:
interface AnInterface{ fun doSomethingInAnObject() } object AnObject : AnInterface{ /** * This is a member-function */ override fun doSomethingInAnObject(){ println("I did it in an object") } } AnObject.doSomethingInAnObject()
In [ ]:
fun doSomethingSpecial(){ val base = "base" val special = "special" /** * This one is inside another function */ fun String.doSomethingInside(){ println("I did $special inside another function on $this") } base.doSomethingInside() } //"ddd".doSomethingInside() doSomethingSpecial()
In [ ]:
fun returnFunction(): (String) -> Unit { fun printStringWithPrefix(str: String){ println("Prefixed: $str") } return ::printStringWithPrefix //return { printStringWithPrefix(it) } // return { println("Prefixed: $it") } } returnFunction()("Haskel, Boo!")
Unit¶
In [ ]:
fun returnUnit(): Unit { // no return statement `Unit` is returned implicitly val b = Unit }
In [ ]:
fun doNothing(){ } val a = doNothing() a::class
Default parameters¶
In [ ]:
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
In [ ]:
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, PI) //error integrate(0.0, step = 0.02, to = PI) { sin(it) } //integrate(0.0, step = 0.02, PI) { sin(it) } integrate(0.0, step = 0.02, function = { sin(it) }, to = PI)
In [ ]:
fun functionWithParameters(){ println("without default") } //@JvmOverloads @JvmName("functionWithDefaultParameters") fun functionWithParameters(a: Int = 2){ println("with default") } functionWithParameters()
Function body shortcut¶
In [ ]:
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
In [ ]:
data class BuildResult internal constructor(val str: String, val int: Int, val optional: Double?){ init{ require(optional!= null){ "null!" } } } class Builder( var str: String = "", var int: Int = 0 ){ var optional: Double? = null fun build() = BuildResult(str, int, optional) } fun BuildResult(defaultInt: Int, block: Builder.() -> Unit): BuildResult = Builder(int = defaultInt).apply(block).build() val res = BuildResult(2){ str = "ff" optional = 1.0 } res
Inheritance¶
In [ ]:
//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()
In [ ]:
fun interface Float64Function: (Double) -> Double { override operator fun invoke(arg: Double): Double operator fun invoke(doubles: DoubleArray): DoubleArray = DoubleArray(doubles.size){ invoke( doubles[it]) } } val sin = object : Float64Function{ override fun invoke(p1: Double): Double = sin(p1) } val cos = Float64Function{ kotlin.math.cos(it) } val tg = Float64Function(::tan) sin(PI/2) sin(doubleArrayOf(0.0, PI/2))
Declaration site variance¶
In consumes, out produces.
In [ ]:
interface Producer<out T>{ fun produce(): T } interface Consumer<in T>{ fun consume(value: T) } interface Doer<T>: Producer<T>, Consumer<T> { }
In [ ]:
fun transform(color: String): Int = when (color) { "Red" -> 0 "Green" -> 1 "Blue" -> 2 else -> throw IllegalArgumentException("Invalid color param value") }
In [ ]:
fun transformIf(flag: Boolean): Int = if (flag) 1 else 0
Runtime type dispatch¶
In Java it is called DOP (Data Oriented Programming).
Also, sometimes (wrongly) called pattern matching.
In [ ]:
/** * Matching by type */ fun checkType(arg: Any): Unit = when (arg) { is String -> println("I am a String. Length is ${arg.length}") 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
In [ ]:
fun tryCatch() { val result: Int = try { 22 } catch (e: ArithmeticException) { throw IllegalStateException(e) } finally { println("finally") } // Working with result }
In [ ]:
val res: Result<Int> = runCatching<Int> { //error("Error happened") 4 } println(res.exceptionOrNull()) (res.getOrNull() ?: 0) + 1
In [ ]:
val dataFlow = sequence { var i = 0 while (true) { yield(i++) } } dataFlow.map { runCatching { if (it % 2 == 0) error("Even: $it") else it } }.take(10).toList().joinToString(separator = "\n")
Nothing¶
In [ ]:
/** * 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 } //FIXME don't do that fun dontDoThat() { if (true) return println(false) } fun checkCondition(): Int { fun conditionSatisfied() = false return if (conditionSatisfied()) { 1 } else { //error is Nothing error("Condition is not satisfied") } }
Data class¶
In [ ]:
class SimpleClass(val a: Int, val b: Double, c: Double){ override fun toString() = "SimpleClass(a=$a, b=$b)" } data class DataClass(val a: Int, var b: Double /*, d: 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)
Nullable truth¶
In [ ]:
/** * Nullable truth. */ fun nullableTruth() { 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 } } }
In [ ]:
import java.util.Optional import kotlin.reflect.typeOf println(typeOf<Optional<Boolean>>() == typeOf<Optional<Optional<Boolean>>>()) println( typeOf<Boolean?>() == typeOf<Boolean??>())
In [ ]:
mapOf<String,String>().get("f")!!
Safe call¶
In [ ]:
/** * Safe call and elvis operator */ fun idiom12() { val files = File("Test").listFiles() println(files?.size ?: "empty") } idiom12()
In [ ]:
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 }
Nullable assignment¶
In [ ]:
/** * Dart-like (?=) nullable assignment */ fun idiom11() { var x: String? = null x = x ?: "Hello" }
Extension functions¶
In [ ]:
/** * 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(): Int = TODO("Don't do that") fun ((Int) -> Int).evalAndIncrement(): (Int) -> Int = { this.invoke(it) + 1 } // val intFunction: (Int)->Int = {it -1} // val modifiedFunction = intFunction.evalAndIncrement() // println(modifiedFunction(0)) "вылысыпыдыстычка".countOs().printMe()
In [ ]:
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
In [ ]:
In [ ]:
fun List<String>.concat() = joinToString(separator = "") listOf("a","b","c").concat() listOf(1,2,3).concat()
In [ ]:
In [ ]:
val functionWithReciever: List<String>.(arg: Int) -> Unit = {arg-> println(get(arg)) } listOf("1", "2", "3").functionWithReciever(1)
Extension properties¶
Remember property usage guidelines.
In [ ]:
/** * Extension property (must be virtual) */ val List<Number>.odd get() = filter { it.toInt() % 2 == 1 } List(10){it}.odd
In [ ]:
var MutableMap<String, String>.a: String? get() = this.get("a") set(value){ if(value == null){ this.remove("a") } else { this.set("a",value) } } val map = mutableMapOf("a" to "a", "b" to "b") map.a = "6" map.a
In [ ]:
/** * 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)¶
In [ ]:
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 = ${this.a}, b = $b, c = $c") /*return@with*/ "some value" } res
In [ ]:
object AContext{ fun AClass.abc() = a + b + c // warning additional concatenation } fun printAbc(aClass: AClass) = with(AContext){ aClass.abc() }
In [ ]:
//using `run` getAClass()?.takeIf { it.a.isNotEmpty() }?.run { // the same as above println("a = $a, b= $b, c = $c") 2 } val runResult: Int = run { } //Using `let` to compose result. Not recommended using without a need val letResult = getAClass()?.let { arg -> arg.c + arg.a }
In [ ]:
//Don't do that fun scopeAbuse(str: String?) = str.takeIf { it?.isNotEmpty() == true }?.let { it.substring(0..4) }?.let { it.matches(".*".toRegex())}
Scope functions (also, apply)¶
In [ ]:
/** * Using apply/also to add a finalizing action */ var i = 2 /** * [also] block does not affect the result */ fun getAndIncrement() = i.also { i += 1 } //don't do that println(getAndIncrement()) println(i)
In [ ]:
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 } fun Rectangle(block: Rectangle.() -> Unit): Rectangle = Rectangle().apply(block) val myRectangle2 = Rectangle { length = 4 breadth = 5 color = 0xFAFAFA } fun Rectangle(length: Number) = Rectangle().apply { this.length = length } Rectangle(8)
Lists¶
Do not cast List to MutableList!
In [ ]:
/** * 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: List<String> = listOf("a", "b", "c") println(list[0]) println(list.get(1)) //println(list.get(4)) println(list.getOrNull(4) ?: "nothing") list::class
In [ ]:
/** * This one creates a mutable list */ val mutableList: MutableList<String> = mutableListOf("a", "b", "c") mutableList[2] = "d" mutableList.add("e") mutableList += "f" mutableList
In [ ]:
//don't do that ever fun doBadThingWithList(list: List<String>): List<String> = (list as MutableList<String>).apply { add("something") } doBadThingWithList(listOf("a", "b", "c"))
In [ ]:
listOf("a")::class
In [ ]:
val mutableList = mutableListOf(1,2,3) fun consumeList(list: List<Int>){ println(list.joinToString()) } consumeList(mutableList) mutableList.add(4) consumeList(mutableList)
In [ ]:
listOf(1,2) + listOf(3,4)
In [ ]:
var roList = listOf("a", "b") roList += setOf("c") roList
In [ ]:
val writableList = mutableListOf("a", "b") // Warning! writableList += setOf("c") writableList
In [ ]:
/** * This one creates a mutable ArrayList. */ val arrayList: ArrayList<String> = arrayListOf("a", "b", "c") //ArrayList<Int>() //Danger zone // //val newList: List<String> = list + "f" + mutableList // //println(newList)
In [ ]:
//Bonus val lambdaList = List(3){ it.toString() } println(lambdaList) val builderList: List<Int> = buildList { add(2) add(8) remove(8) } builderList
In [ ]:
// val sequentialList = List(20){it} // sequentialList.zipWithNext{ l, r -> r - l }.forEach{println(it)}
Shortcut collection builders¶
In [ ]:
/** * Use shortcut function to provide default values */ fun doSomething(additionalArguments: List<String> = emptyList()){ TODO() emptyArray<String>() emptySet<String>() emptyMap<String,String>() } emptyList<String>()::class
In [ ]:
Maps¶
In [ ]:
val map = mutableMapOf( "key" to "a", "key2" to "b", ) //The map could be accessed via indexing operation println(map::class) println(map["key"]) map["key"] = "fff" println(map["key"]) println(map["key3"])
In [ ]:
//val entry: MutableMap.MutableEntry<String, String> = map.iterator().next() //map.entries.first().component2() /** * The destructuring declaration for maps and other objects that support `operator fun componentN` */ for ((k: String, v) in map) { //val (k, v) = entry // val k = entry.component1() // val v = entry.component2() println("$k -> $v") }
In [ ]:
map.forEach { (k, v) -> println("$k -> $v")}
In [ ]:
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
In [ ]:
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 fun add(int: Int){ require(int>0) _list.add(int) } } val obj = AClassWithList() // obj.b = 10.0 //error obj.add(42) obj.list.add(43)
Wrap mutable logic¶
In [ ]:
val list = buildList { repeat(10){ add(it) } } println(list)
In [ ]:
data class ImmutableObject(val a: String, val b: String, val c: Int) class ImmutableObjectBuilder(a: String) { var a: String = a var b: String = "" var c = 0 fun build() = ImmutableObject(a, b, c) } fun ImmutableObject(a: String, block: ImmutableObjectBuilder.() -> Unit): ImmutableObject = ImmutableObjectBuilder(a).apply(block).build() ImmutableObject("aValue") { c = 99 }
Contains operator and ranges¶
In [ ]:
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") }
In [ ]:
import java.time.* 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))
In [ ]:
// 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¶
In [ ]:
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
In [ ]:
val sequence = sequence { var counter = 1 while(true){ yield(counter++) 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
Scoped resource usage¶
In [ ]:
import java.nio.file.* 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¶
In [ ]:
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()) fun buildSpecial(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¶
In [ ]:
open class Bad{ val value: Int = requestValue() open fun requestValue(): Int { doSomethingElse() return 2 } private fun doSomethingElse(){ println(value) } } Bad()
In [ ]:
class BadString{ val value: String = requestValue() fun requestValue(): String { doSomethingElse() return "2" } private fun doSomethingElse(){ println(value) } } BadString()
In [ ]:
//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¶
In [ ]:
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
In [ ]:
//Using other delegates val map = mutableMapOf("a" to 1, "b" to 2) var a: Int by map println(a) a = 3 println(map)
Inline functions¶
In [ ]:
/** * 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(): Int { listOf(1, 2, 3, 4, 5).forEachOdd { if (it == 3) return it // non-local return directly to the caller of foo() print("$it, ") } println("this point is unreachable") return 0 } foo()
In [ ]:
/** * 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() listOf(1, 2, 3).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¶
In [ ]:
/** * 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() }
In [ ]: