Moskala M. Functional Kotlin 2023
Moskala M. Functional Kotlin 2023
Marcin Moskała
This book is for sale at
http://leanpub.com/kotlin_functional
Introduction 1
Who is this book for? 1
What will be covered? 1
The Kotlin for Developers series 2
Code conventions 3
Acknowledgments 5
Introduction to functional programming with Kotlin 7
Why do we need to use functions as objects? 10
Function types 14
Defining function types 14
Using function types 15
Named parameters 18
Type aliases 19
A function type is an interface 21
Anonymous functions 23
Lambda expressions 27
Tricky braces 27
Parameters 29
Trailing lambdas 31
Result values 32
Lambda expression examples 35
An implicit name for a single parameter 37
Closures 38
Lambda expressions vs anonymous functions 38
Function references 41
Top-level functions references 41
Method references 44
Extension function references 45
Method references and generic types 47
CONTENTS
Introduction
• function types,
• anonymous functions,
• lambda expressions,
• function references,
• functional interfaces,
Introduction 2
Code conventions
fun main() {
val cheer: () -> Unit = fun() {
println("Hello")
}
cheer.invoke() // Hello
cheer() // Hello
}
fun main() {
val cheer: () -> Unit = fun() {
println("Hello")
}
cheer.invoke()
cheer()
}
// Hello
// Hello
adapter.setOnSwipeListener { /*...*/ }
Introduction 5
Acknowledgments
Kotlin has powerful support for many features that are typ-
ical to functional programming languages. Let’s consider
our previous list of concepts that are typical of functional
programming and let’s look at how Kotlin supports them.
Feature Support
Treating functions as Function types, lambda
objects expressions, function
references
Higher-order functions Full support
Data immutability val support, default
collections are
read-only, copy in data
classes
Expressions if-else, try-catch, when
statements are
expressions
lazy evaluation lazy delegate.
fun main() {
sum(5, 10) // 45
product(5, 10) // 151200
}
The first one calculates the sum of all the numbers in a range;
the second one calculates a product. The bodies of these
two functions are nearly identical: the only difference is in
the initial value and the operation. Yet, without support for
treating functions as objects, extracting the common parts
would not make any sense. Just think about how it would
look in Java before version 8. In such a case, we had to create
classes to represent operations and interfaces to specify what
you expect… It would be absurd.
Introduction to functional programming with Kotlin 11
// Java 7
public class RangeOperations {
fun fold(
a: Int,
b: Int,
initial: Int,
operation: (Int, Int) -> Int
): Int {
var acc = initial
for (i in a..b) {
acc = operation(acc, i)
}
return acc
}
In this book, we will learn this and much more. Does it sound
interesting? So, let’s get started.
Function types 14
Function types
Here are a few function types (in the next chapters, we will see
them in use):
Functions that return Boolean, like (T) -> Boolean, are often
named predicate. Functions that transform one value to an-
other, like (T) -> R, are often called transformation. Functions
that return Unit, like (T) -> Unit, are often called operation.
fun fetchText(
onSuccess: (String) -> Unit,
onFailure: (Throwable) -> Boolean
) {
// ...
onSuccess.invoke("Some text") // returns Unit
// or
val handled: Boolean =
onFailure.invoke(Error("Some error"))
}
fun fetchText(
onSuccess: (String) -> Unit,
onFailure: (Throwable) -> Boolean
) {
// ...
onSuccess("Some text") // returns Unit
// or
val handled: Boolean = onFailure(Error("Some error"))
}
You can decide for yourself what approach you prefer. Explicit
invoke calls are more readable for less experienced developers.
An implicit call is shorter and, from a conceptual perspective,
it better represents calling an object.
If a function type is nullable (in such a case, wrap it with a
bracket and add a question mark at the end), you can use a safe
call only with an explicit invoke.
fun someOperations(
onStart: (() -> Unit)? = null,
onCompletion: (() -> Unit)? = null,
) {
onStart?.invoke()
// ...
onCompletion?.invoke()
}
Named parameters
fun setListItemListener(
listener: (Int, Int, View, View) -> Unit
) {
listeners = listeners + listener
}
fun setListItemListener(
listener: (
position: Int,
id: Int,
child: View,
parent: View
) -> Unit
) {
listeners = listeners + listener
}
Such names are visible in IntelliJ hints, and they have sug-
gested names when we define a lambda expression for this
type.
Function types 19
Type aliases
fun setListItemListener(
listener: (
position: Int, id: Int,
View, parent: View
) -> Unit
) {
listeners = listeners + listener
}
fun removeListItemListener(
listener: (Int, Int, View, View) -> Unit
) {
listeners = listeners - listener
}
fun main() {
val users: Users = emptyList()
// during compilation becomes
// val users: List<User> = emptyList()
import thirdparty.Name
class Foo {
val name1: Name
val name2: my.Name
}
import my.Name
class Foo {
val name1: ThirdPartyName
val name2: Name
}
fun main() {
val time = decideAboutTime()
setupTimer(time)
}
Under the hood, all function types are just interfaces with
generic type parameters. This is why a class can implement
a function type.
fun main() {
val onClick = OnClick()
setListener(onClick)
}
Anonymous functions
fun main() {
val cheer: () -> Unit = fun() {
println("Hello")
}
cheer.invoke() // Hello
cheer() // Hello
Lambda expressions
Tricky braces
fun main() {
val f: () -> Unit = {}
f()
// or f.invoke()
}
But be careful because all braces that are not part of a Kotlin
structure are lambda expressions (we can call them orphaned
lambda expressions). This can lead to a lot of problems. Take
a look at the following example: What does the following main
function print?
Lambda expressions 28
fun main() {
{
println("AAA")
}
}
fun produce() = { 42 }
fun main() {
println(produce()) // ???
}
fun produceFun() = { 42 }
fun produceNum() = 42
fun main() {
val f = produceFun()
println(f()) // 42
println(produceFun()()) // 42
println(produceFun().invoke()) // 42
println(produceNum()) // 42
}
Lambda expressions 29
Parameters
fun main() {
val printTimes = { text: String, times: Int ->
for (i in 1..times) {
print(text)
}
} // the type is (text: String, times: Int) -> Unit
printTimes("Na", 7) // NaNaNaNaNaNaNa
printTimes.invoke("Batman", 2) // BatmanBatman
}
fun main() {
setOnClickListener({ view, click ->
println("Clicked")
})
}
setOnClickListener({ _, _ ->
println("Clicked")
})
fun main() {
setOnClickListener({ (name, surname), (id, type) ->
println(
"User $name $surname clicked " +
"element $id of type $type"
)
})
}
Trailing lambdas
fun main() {
run({ println("A") }) // A
run() { println("A") } // A
run { println("A") } // A
repeat(2, { print("B") }) // BB
println()
repeat(2) { print("B") } // BB
}
setOnClickListener { _, _ ->
println("Clicked")
}
But be careful because this convention works only for the last
parameter. Take a look at the snippet below and guess what
will be printed.
fun main() {
call({ print("C") })
call { print("B") }
}
fun main() {
call(before = { print("C") })
call(after = { print("B") })
}
Result values
fun main() {
val f = {
10
20
30
}
println(f()) // 30
}
fun main() {
onUserChanged { user ->
if (user == null) return // compilation error
cheerUser(user)
}
}
fun main() {
onUserChanged someLabel@{ user ->
if (user == null) return@someLabel
cheerUser(user)
}
}
Lambda expressions 34
fun main() {
onUserChanged { user ->
if (user == null) return@onUserChanged
cheerUser(user)
}
}
fun main() {
val magicSquare = listOf(
listOf(2, 7, 6),
listOf(9, 5, 1),
listOf(4, 3, 8),
)
magicSquare.forEach line@ { line ->
var sum = 0
line.forEach { elem ->
sum += elem
if (sum == 15) {
return@line
}
}
print("Line $line not correct")
}
}
fun main() {
val cheer: () -> Unit = {
println("Hello")
}
cheer.invoke() // Hello
cheer() // Hello
val cheer = {
println("Hello")
}
val printNumber = { i: Int ->
println(i)
}
val log = { ctx: String, message: String ->
println("[$ctx] $message")
}
val makeAdmin = { User(id = 0) }
val add = { s1: String, s2: String -> s1 + s2 }
val toName = { name: String -> Name(name) }
Closures
fun main() {
val counter1 = makeCounter()
val counter2 = makeCounter()
println(counter1()) // 0
println(counter1()) // 1
println(counter2()) // 0
println(counter1()) // 2
println(counter1()) // 3
println(counter2()) // 1
}
data.uppercase()
}
return data.uppercase()
}
Function references
fun makeComplex(
real: Double = 0.0,
imaginary: Double = 0.0
) = Complex(real, imaginary)
fun main() {
val f = ::add // function reference
println(f.isOpen) // false
println(f.visibility) // PUBLIC
// The above statements require `kotlin-reflect`
// dependency
}
fun main() {
val f: (Int, Int) -> Int = ::add
// an alternative to:
// val f: (Int, Int) -> Int = { a, b -> add(a, b) }
println(f(10, 20)) // 30
}
Let’s get back to our basic functions. Can you guess what the
function type of zeroComplex and makeComplex should be?
¹³More about reflection in Advanced Kotlin, Reflection chap-
ter.
¹⁴For this, the reference needs to be immediately typed as a
function type.
Function references 43
fun makeComplex(
real: Double = 0.0,
imaginary: Double = 0.0
) = Complex(real, imaginary)
fun main() {
val f1: () -> Complex = ::zeroComplex
println(f1()) // Complex(real=0.0, imaginary=0.0)
Method references
fun main() {
val numberObject = Number(10)
// member function reference
val float: (Number) -> Float = Number::toFloat
// `toFloat` has no parameters, but its function type
// needs a receiver of type `Number`
println(float(numberObject)) // 10.0
val multiply: (Number, Int) -> Number = Number::times
println(multiply(numberObject, 4)) // Number(num = 40.0)
// `times` has one parameter of type `Int`, but its
// function type also needs a receiver of type `Number`
}
Getting back to our basic functions, can you deduce the func-
tion type of Complex::doubled and Complex::times?
doubled has no explicit parameters, a receiver of type Complex,
and the result type is Complex; therefore, the function type of
its function reference is (Complex) -> Complex. times has an
explicit parameter of type Int, a receiver of type Complex, and
the result type is Complex; therefore, the function type of its
function reference is (Complex, Int) -> Complex.
fun main() {
val c1 = Complex(1.0, 2.0)
fun main() {
val num = Number(10)
// extension function reference
val float: (Number) -> Float = Number::toFloat
println(float(num)) // 10.0
val multiply: (Number, Int) -> Number = Number::times
println(multiply(num, 4)) // Number(num = 40.0)
}
fun main() {
val c1 = Complex(1.0, 2.0)
val c2 = Complex(4.0, 5.0)
fun main() {
val teamPoints = TeamPoints(listOf(1, 3, 5))
fun main() {
val unbox = Box<String>::unbox
val box = Box("AAA")
println(unbox(box)) // AAA
}
fun main() {
val num = Number(10)
// bounded function reference
val getNumAsFloat: () -> Float = num::toFloat
// There is no need for receiver type in function type,
// because reference is already bound to an object
println(getNumAsFloat()) // 10.0
val multiplyNum: (Int) -> Number = num::times
println(multiplyNum(4)) // Number(num = 40.0)
}
Function references 49
fun main() {
val c1 = Complex(1.0, 2.0)
object SuperUser {
fun getId() = 0
}
fun main() {
val myId = SuperUser::getId
println(myId()) // 0
class MainPresenter(
private val view: MainView,
private val repository: MarvelRepository
) : BasePresenter() {
fun onViewCreated() {
subscriptions += repository.getAllCharacters()
.applySchedulers()
.subscribeBy(
onSuccess = this::show,
onError = view::showError
)
}
Constructor references
fun main() {
// constructor reference
val produce: (Double, Double) -> Complex = ::Complex
println(produce(1.0, 2.0))
// Complex(real=1.0, imaginary=2.0)
}
fun main() {
val ints: List<Int> = listOf(1, 1, 2, 3, 5, 8)
val studentIds: List<StudentId> = ints.map(::StudentId)
val userIds: List<UserId> = studentIds.map(::UserId)
}
object Robot {
fun moveForward() {
/*...*/
}
fun moveBackward() {
/*...*/
}
}
fun main() {
Robot.moveForward()
Robot.moveBackward()
class Drone {
fun setOff() {}
fun land() {}
companion object {
fun makeDrone(): Drone = Drone()
}
}
fun main() {
val maker: () -> Drone = Drone.Companion::makeDrone
}
Function references 54
fun main() {
println(foo(123)) // 1
println(foo("")) // AAA
}
fun main() {
val fooInt: (Int) -> Int = ::foo
println(fooInt(123)) // 1
val fooStr: (String) -> String = ::foo
println(fooStr("")) // AAA
}
fun main() {
val intToUserId: (Int) -> UserId = ::UserId
println(intToUserId(1)) // UserId(value=1)
Property references
fun main() {
val c1 = Complex(1.0, 2.0)
val c2 = Complex(3.0, 4.0)
// property reference
val getter: (Complex) -> Double = Complex::real
println(getter(c1)) // 1.0
println(getter(c2)) // 3.0
For var, you can reference the setter using the setter property
from the property reference, but this requires kotlin-reflect;
therefore, I recommend avoiding this approach because it
might impact your code’s performance.
There are many kinds of references. Some developers like
using them, while others avoid them. Anyway, it is good to
know how function references look and behave. It is worth
practicing them as they can help make our code more elegant
in applications where functional programming concepts are
widely used.
SAM Interface support in Kotlin 57
interface OnClick {
fun onClick(view: View)
}
setOnClickListener(object : OnClick {
override fun onClick(view: View) {
// ...
}
})
// OnSwipeListener.java
public interface OnSwipeListener {
void onSwipe();
}
// ListAdapter.java
public class ListAdapter {
// kotlin
val adapter = ListAdapter()
adapter.setOnSwipeListener { /*...*/ }
val listener = OnSwipeListener { /*...*/ }
adapter.setOnSwipeListener(listener)
SAM Interface support in Kotlin 59
adapter.setOnSwipeListener(fun() { /*...*/ })
adapter.setOnSwipeListener(::someFunction)
Functional interfaces
// kotlin
fun setOnClickListener(listener: (Action) -> Unit) {
//...
}
// Kotlin usage
setOnClickListener { /*...*/ }
val listener = OnClick { /*...*/ }
setOnClickListener(listener)
setOnClickListener(fun(view) { /*...*/ })
setOnClickListener(::someFunction)
// ...
@Override
public void onClick(@NotNull View view) {
/*...*/
}
});
interface ElementListener<T> {
fun invoke(element: T)
}
• Java interoperability,
• optimization for primitive types,
• when we need to not only represent a function but also
to express a concrete contract.
Inline functions
fun main() {
val points = students.fold(0) { acc, s -> acc + s.points }
println(points)
}
fun main() {
var points = 0
for (student in students) {
points += student.points
}
println(points)
}
Inline functions
fun main() {
print("A")
print("B")
print("C")
}
System.out.print("A")
System.out.print("B")
System.out.print("C")
}
fun main() {
repeat(10) {
print(it)
}
}
Since repeat is replaced with its body, and the lambda expres-
sion’s body is inlined into its usage, the compiled code will be
the equivalent of the following:
Inline functions 65
fun main() {
for (index in 0 until 10) {
print(index)
}
}
fun main() {
val points = students.fold(0) { acc, s -> acc + s.points }
println(points)
}
The result is not only more efficient, but also fewer objects are
allocated.
Inline functions 66
It is like having your cake and eating it! So, it’s no wonder that
it has become standard practice to mark top-level functions
with functional parameters as inline. Here are some exam-
ples:
Non-local return
fun main() {
repeat(7) {
print("Na")
}
println(" Batman")
}
// NaNaNaNaNaNaNa Batman
fun main() {
for (i in 0 until 10) {
if (i == 4) return // Returns from main
print(i)
}
}
// 0123
fun main() {
repeat(10) { index ->
if (index == 4) return // Returns from main
print(index)
}
}
// 0123
fun main() {
for (index in 0 until 10) {
if (index == 4) return // Returns from main
print(index)
}
}
// 0123
fun main() {
(0 until 19).forEach { index ->
if (index == 4) return // Returns from main
print(index)
}
}
// 0123
fun main() {
printTypeName<Int>() // Int
printTypeName<Char>() // Char
printTypeName<String>() // String
}
fun main() {
print(Int::class.simpleName) // Int
print(Char::class.simpleName) // Char
print(String::class.simpleName) // String
}
class Worker
class Manager
// usage
val user: User? = userAsText.fromJsonOrNull()
Below are examples of how the Koin library uses reified func-
tions to simplify both dependency injection and module dec-
laration.
// Koin injection
val service: BusinessService by inject()
// inject is reified
Inline functions 73
Inline properties
fun main() {
val user = User("A", "B")
println(user.fullName) // A B
// during compilation changes to
println("${user.name} ${user.surname}")
}
Collection processing
// better
users.filter { it.isActive }
.flatMap { it.remainingMessages }
.filter { it.isToBeSent }
.forEach { sendMessage(it) }
users
.filter { it.isActive }
.onEach { log("Sending messages for user $it") }
.flatMap { it.remainingMessages }
.filter { it.isToBeSent }
.forEach { sendMessage(it) }
Collection processing 82
filter
fun main() {
val old = listOf(1, 2, 6, 11)
val new = old.filter { it in 2..10 }
println(new) // [2, 6]
}
Collection processing 84
fun main() {
val old = listOf(1, 2, 6, 11)
val new = old.filterNot { it in 2..10 }
println(new) // [1, 11]
}
Collection processing 85
map
fun main() {
val old = listOf(1, 2, 3, 4)
val new = old.map { it * it }
println(new) // [1, 4, 9, 16]
}
Collection processing 86
fun main() {
val names: List<String> = listOf("Alex", "Bob", "Carol")
val nameSizes: List<Int> = names.map { it.length }
println(nameSizes) // [4, 3, 5]
}
flatMap
fun main() {
val old = listOf(1, 2, 3)
val new = old.flatMap { listOf(it, it + 10) }
println(new) // [1, 11, 2, 12, 3, 13]
}
Collection processing 89
fun main() {
val names = listOf("Ann", "Bob", "Cale")
val chars1: List<Char> = names.flatMap { it.toList() }
println(chars1) // [A, n, n, B, o, b, C, a, l, e]
val mapRes: List<List<Char>> = names.map { it.toList() }
println(mapRes) // [[A, n, n], [B, o, b], [C, a, l, e]]
val chars2 = mapRes.flatten()
println(chars2) // [A, n, n, B, o, b, C, a, l, e]
println(chars1 == chars2) // true
}
fold
Since you can specify the initial value, you can decide the
result type. If your initial value is an empty string and your
operation is addition, then the result will be a “1234” string.
Collection processing 92
fun main() {
val numbers = listOf(1, 2, 3, 4)
val sum = numbers.fold(0) { acc, i -> acc + i }
println(sum) // 10
val joinedString = numbers.fold("") { acc, i -> acc + i }
println(joinedString) // 1234
val product = numbers.fold(1) { acc, i -> acc * i }
println(product) // 24
}
On the other hand, thanks to the fact that the Kotlin standard
library has so many collection processing functions, we rarely
need to use fold. Even the functions we presented before that
calculate a sum and join elements into a string have dedicated
methods.
Collection processing 93
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.sum()) // 15
println(numbers.joinToString(separator = "")) // 12345
}
In some situations, you might want to have not only the result
of fold accumulations but also all the intermediate values. For
that, you can use the runningFold method or its alias²³ scan.
²³In this chapter, by aliases we will mean functions with
exactly the same meaning.
Collection processing 94
fun main() {
val numbers = listOf(1, 2, 3, 4)
println(numbers.fold(0) { acc, i -> acc + i }) // 10
println(numbers.scan(0) { acc, i -> acc + i })
// [0, 1, 3, 6, 10]
println(numbers.runningFold(0) { acc, i -> acc + i })
// [0, 1, 3, 6, 10]
reduce
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.fold(0) { acc, i -> acc + i }) // 15
println(numbers.reduce { acc, i -> acc + i }) // 15
sum
fun main() {
val numbers = listOf(1, 6, 2, 4, 7, 1)
println(numbers.sum()) // 21
import java.math.BigDecimal
fun main() {
val players = listOf(
Player("Jake", 234, BigDecimal("2.30")),
Player("Megan", 567, BigDecimal("1.50")),
Player("Beth", 123, BigDecimal("0.00")),
)
fun main() {
listOf("A", "B", "C", "D") // List<String>
.withIndex() // List<IndexedValue<String>>
.filter { (index, value) -> index % 2 == 0 }
.map { (index, value) -> "[$index] $value" }
.forEach { println(it) }
}
// [0] A
// [2] C
fun main() {
val chars = listOf("A", "B", "C", "D")
element, and this index stays the same for all steps, while the
indexed function operates on the current index for each step.
fun main() {
val chars = listOf("A", "B", "C", "D")
val r1 = chars.withIndex()
.filter { (index, value) -> index % 2 == 0 }
.map { (index, value) -> "[$index] $value" }
println(r1) // [[0] A, [2] C]
val r2 = chars
.filterIndexed { index, value -> index % 2 == 0 }
.mapIndexed() { index, value -> "[$index] $value" }
println(r2) // [[0] A, [1] C]
}
fun main() {
val chars = ('a'..'z').toList()
println(chars.take(10))
// [a, b, c, d, e, f, g, h, i, j]
println(chars.takeLast(10))
// [q, r, s, t, u, v, w, x, y, z]
println(chars.drop(10))
// [k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z]
println(chars.dropLast(10))
// [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]
}
Collection processing 103
Collection processing 104
fun main() {
val c = ('a'..'z').toList()
fun main() {
val c = ('a'..'z').toList()
val n = 10
val s = c.size
println(c.take(n) == c.subList(0, n)) // true
println(c.takeLast(n) == c.subList(s - n, s)) // true
println(c.drop(n) == c.subList(n, s)) // true
println(c.dropLast(n) == c.subList(0, s - n)) // true
}
Collection processing 106
fun main() {
val letters = listOf("a", "b", "c")
println(letters.take(100)) // [a, b, c]
println(letters.takeLast(100)) // [a, b, c]
println(letters.drop(100)) // []
println(letters.dropLast(100)) // []
letters.subList(0, 4) // throws IndexOutOfBoundsException
}
Collection processing 107
If you need to get the first element of a collection, use the first
method. To get the last one, use the last method. To find an
element at a concrete index, use the get function, which is
also an operator and can be replaced with box brackets. You
can also destructure a list into elements, starting at the first
position.
fun main() {
val c = ('a'..'z').toList()
println(c.first()) // a
println(c.last()) // z
println(c.get(3)) // d
println(c[3]) // d
val (c1, c2, c3) = c
println(c1) // a
println(c2) // b
println(c3) // c
}
Collection processing 108
fun main() {
val c = listOf<Char>()
println(c.firstOrNull()) // null
println(c.lastOrNull()) // null
println(c.getOrNull(3)) // null
}
Collection processing 109
Finding an element
find is just an alias for firstOrNull. They both return the first
element that fulfills the predicate, or null if no such element
is found.
fun main() {
val names = listOf("Cookie", "Figa")
If you prefer to start searching from the end, you can use
findLast or lastOrNull.
fun main() {
val names = listOf("C1", "C2")
Counting: count
fun main() {
val range = (1..100 step 3)
println(range.count()) // 34
}
fun main() {
val range = (1..100 step 3)
println(range.count { it % 5 == 0 }) // 7
}
fun main() {
val people = listOf(
Person("Alice", 31, false),
Person("Bob", 29, true),
Person("Carol", 31, true)
)
// Is there an adult?
println(people.any(::isAdult)) // true
// Are they all adults?
println(people.all(::isAdult)) // true
// Is none of them an adult?
println(people.none(::isAdult)) // false
fun main() {
val emptyList = emptyList<String>()
println(emptyList.any { error("Ignored") }) // false
println(emptyList.all { error("Ignored") }) // true
println(emptyList.none { error("Ignored") }) // true
}
²⁵To learn more about this, search under the term “vacuous
truth”.
Collection processing 117
partition
fun main() {
val nums = listOf(1, 2, 6, 11)
val partitioned: Pair<List<Int>, List<Int>> =
nums.partition { it in 2..10 }
println(partitioned) // ([2, 6], [1, 11])
fun main() {
val nums = (1..10).toList()
groupBy
fun main() {
val names = listOf("Marcin", "Maja", "Cookie")
You can reverse the groupBy method using flatMap. If you first
use groupBy and then flatMap the values, you will have the same
elements you started with (but possibly in a different order).
fun main() {
val players = listOf(
Player("Alex", "A"),
Player("Ben", "B"),
Player("Cal", "A"),
)
val grouped = players.groupBy { it.team }
println(grouped)
// {A=[Player(name=Alex, team=A),
// Player(name=Cal, team=A)],
// B=[Player(name=Ben, team=B)]}
println(grouped.flatMap { it.value })
// [Player(name=Alex, team=A), Player(name=Cal, team=A),
// Player(name=Ben, team=B)]
}
fun main() {
val names = listOf("Alex", "Ben", "Cal")
println(names.associate { it.first() to it.drop(1) })
// {A=lex, B=en, C=al}
println(names.associateWith { it.length })
// {Alex=4, Ben=3, Cal=3}
println(names.associateBy { it.first() })
// {A=Alex, B=Ben, C=Cal}
}
²⁸I hope it is clear, that List and Set are iterables, because
they implement Iterable interface.
Collection processing 124
fun main() {
val names = listOf("Alex", "Aaron", "Ada")
println(names.associateBy { it.first() })
// {A=Ada}
println(names.groupBy { it.first() })
// {A=[Alex, Aaron, Ada]}
}
fun main() {
val names = listOf("Alex", "Ben", "Cal")
val aW = names.associateWith { it.length }
println(aW.keys.toList() == names) // true
val aB = names.associateBy { it.first() }
println(aB.values.toList() == names) // true
}
fun produceUserOffers(
offers: List<Offer>,
users: List<User>
): List<UserOffer> {
//
val usersById = users.associateBy { it.id }
return offers
.map { createUserOffer(it, usersById[it.buyerId]) }
}
fun main() {
val list: List<Int> = listOf(1, 2, 4, 2, 3, 1)
val set: Set<Int> = list.toSet()
println(set) // [1, 2, 4, 3]
}
fun main() {
val numbers = listOf(1, 2, 4, 2, 3, 1)
println(numbers) // [1, 2, 4, 2, 3, 1]
println(numbers.distinct()) // [1, 2, 4, 3]
fun main() {
val names = listOf("Marta", "Maciek", "Marta", "Daniel")
println(names) // [Marta, Maciek, Marta, Daniel]
println(names.distinctBy { it[0] }) // [Marta, Daniel]
println(names.distinctBy { it.length }) // [Marta, Maciek]
}
Be aware that distinct keeps the first element of the list, while
associateBy keeps the last element.
fun main() {
val names = listOf("Marta", "Maciek", "Daniel")
println(names)
// [Marta, Maciek, Daniel]
println(names.distinctBy { it.length })
// [Marta, Maciek]
println(names.associateBy { it.length }.values)
// [Marta, Daniel]
}
fun main() {
val people = listOf(
Person(0, "Alex"),
Person(1, "Ben"),
Person(1, "Carl"),
Person(2, "Ben"),
Person(0, "Alex"),
)
println(people.distinct())
// [0: Alex, 1: Ben, 1: Carl, 2: Ben]
println(people.distinctBy { it.id })
Collection processing 129
fun main() {
println(listOf(4, 1, 3, 2).sorted())
// [1, 2, 3, 4]
fun main() {
val names = listOf("Ben", "Bob", "Bass", "Alex")
val sorted = names.sortedBy { it.first() }
println(sorted) // [Alex, Ben, Bob, Bass]
}
To reverse the order of the elements in the list, use the reversed
method.
fun main() {
println(listOf(4, 1, 3, 2).reversed())
// [2, 3, 1, 4]
println(listOf('C', 'B', 'F', 'A', 'D', 'E').reversed())
// [E, D, A, F, B, C]
}
fun main() {
println(listOf(4, 1, 3, 2).sortedDescending())
// [4, 3, 2, 1]
println(listOf(4, 1, 3, 2).sorted().reversed())
// [4, 3, 2, 1]
println(
listOf('b', 'A', 'a', ' ', 'B')
.sortedDescending()
)
// [b, a, B, A, ]
println(
listOf("Bab", "AAZ", "Bza", "A")
.sortedDescending()
)
// [Bza, Bab, AAZ, A]
fun main() {
val names = listOf("Alex", "Bob", "Celine")
fun main() {
val names = listOf("Alex", "Bob", "Celine")
fun main() {
val people = listOf(
Person(1, "Alex"),
Person(null, "Ben"),
Person(2, null),
)
println(people.sortedBy { it.id })
// [null: Ben, 1: Alex, 2: null]
println(people.sortedBy { it.name })
// [2: null, 1: Alex, null: Ben]
}
fun main() {
val names = listOf(
FullName("B", "B"),
FullName("B", "A"),
FullName("A", "A"),
Collection processing 137
FullName("A", "B"),
)
println(names.sortedBy { it.name })
// [A A, A B, B B, B A]
println(names.sortedBy { it.surname })
// [B A, A A, B B, A B]
println(names.sortedWith(compareBy(
{ it.surname },
{ it.name }
)))
// [A A, B A, A B, B B]
println(names.sortedWith(compareBy(
{ it.name },
{ it.surname }
)))
// [A A, A B, B A, B B]
}
return recommendations.sortedWith(
compareBy(
{ it.blocked }, // blocked to the end
{ !it.favourite }, // favorite at the beginning
{ calculateScore(it) },
)
)
• compareBy,
Collection processing 138
class Student(
val name: String,
val surname: String,
val score: Double,
val year: Int,
) {
companion object {
val ALPHABETICAL_ORDER =
compareBy<Student>({ it.surname }, { it.name })
val BY_SCORE_ORDER =
compareByDescending<Student> { it.score }
val BY_YEAR_ORDER =
compareByDescending<Student> { it.year }
}
}
fun main() {
val list = listOf(4, 2, 3, 1)
val sortedRes = list.sorted()
// list.sort() is illegal
println(list) // [4, 2, 3, 1]
println(sortedRes) // [1, 2, 3, 4]
first sort the elements and then take the first or the last
one, but such a solution would be far from optimal. Instead,
we should use functions that start with the “max” or “min”
prefix.
If we want to find an extreme using the natural order of the
elements, use maxOrNull or minOrNull, both of which return null
when a collection is empty.
fun main() {
val numbers = listOf(1, 6, 2, 4, 7, 1)
println(numbers.maxOrNull()) // 7
println(numbers.minOrNull()) // 1
}
fun main() {
val players = listOf(
Player("Jake", 234),
Player("Megan", 567),
Player("Beth", 123),
)
println(players.maxByOrNull { it.score })
// Player(name=Megan, score=567)
println(players.minByOrNull { it.score })
// Player(name=Beth, score=123)
}
fun main() {
val names = listOf(
FullName("B", "B"),
FullName("B", "A"),
FullName("A", "A"),
FullName("A", "B"),
)
println(
names
.maxWithOrNull(compareBy(
{ it.surname },
{ it.name }
))
)
// FullName(name=B, surname=B)
println(
names
.minWithOrNull(compareBy(
{ it.surname },
{ it.name }
))
)
// FullName(name=A, surname=A)
}
fun main() {
val players = listOf(
Player("Jake", 234),
Player("Megan", 567),
Player("Beth", 123),
)
import kotlin.random.Random
fun main() {
val range = (1..100)
val list = range.toList()
println(list.random(Random(123))) // 7
println(list.randomOrNull(Random(123))) // 7
println(range.shuffled())
// List with numbers in a random order
}
fun main() {
val characters = listOf(
Character("Tamara", "Kurczak"),
Character("Earl", "Gey"),
Character("Ryba", "Luna"),
Character("Cookie", "DePies"),
)
println(characters.random())
// A random character,
// like Character(name=Ryba, surname=Luna)
println(characters.shuffled())
// List with characters in a random order
}
fun main() {
val nums = 1..4
val chars = 'A'..'F'
println(nums.zip(chars))
// [(1, A), (2, B), (3, C), (4, D)]
A still from the movie Pan Tadeusz, directed by Andrzej Wajda, presenting the
polonaise dance.
fun main() {
// zip can be used with infix notation
val zipped = (1..4) zip ('a'..'d')
println(zipped) // [(1, a), (2, b), (3, c), (4, d)]
val (numbers, letters) = zipped.unzip()
println(numbers) // [1, 2, 3, 4]
println(letters) // [a, b, c, d]
}
fun main() {
println((1..4).zipWithNext())
// [(1, 2), (2, 3), (3, 4)]
fun main() {
val person = listOf("A", "B", "C", "D", "E")
println(person.zipWithNext { prev, next -> "$prev$next" })
// [AB, BC, CD, DE]
}
Windowing
needs to fully fall off the collection for the process to stop
(with partialWindows for the process to stop, our trolley can
extend past the end of the collection to include any remaining
elements).
Collection processing 149
fun main() {
val person = listOf(
"Ashley",
"Barbara",
"Cyprian",
"David",
)
println(person.windowed(size = 1, step = 1))
// [[Ashley], [Barbara], [Cyprian], [David]]
// so similar to map { listOf(it) }
println(
person.windowed(
size = 3,
step = 1,
partialWindows = true
)
)
// [[Ashley, Barbara, Cyprian], [Barbara, Cyprian, David],
// [Cyprian, David], [David]]
println(
person.windowed(
size = 3,
step = 2,
partialWindows = true
)
)
// [[Ashley, Barbara, Cyprian], [Cyprian, David]]
}
fun main() {
val person = listOf(
"Ashley",
"Barbara",
"Cyprian",
"David",
)
println(person.chunked(1))
// [[Ashley], [Barbara], [Cyprian], [David]]
println(person.chunked(2))
// [[Ashley, Barbara], [Cyprian, David]]
println(person.chunked(3))
// [[Ashley, Barbara, Cyprian], [David]]
println(person.chunked(4))
// [[Ashley, Barbara, Cyprian, David]]
}
Collection processing 152
joinToString
fun main() {
val names = listOf("Maja", "Norbert", "Ola")
println(names.joinToString())
// Maja, Norbert, Ola
println(names.joinToString { it.uppercase() })
// MAJA, NORBERT, OLA
println(names.joinToString(separator = "; "))
// Maja; Norbert; Ola
println(names.joinToString(limit = 2))
// Maja, Norbert, ...
println(names.joinToString(limit = 2, truncated = "etc."))
// Maja, Norbert, etc.
println(
names.joinToString(
prefix = "{names=[",
postfix = "]}"
)
Collection processing 153
)
// {names=[Maja, Norbert, Ola]}
}
fun main() {
val names: Map<Int, String> =
mapOf(0 to "Alex", 1 to "Ben")
println(names)
// {0=Alex, 1=Ben}
students.shuffled()
.chunked(GROUP_SIZE)
To find the student with the highest result in each group, you
can use groupBy and maxByOrNull.
students.groupBy { it.group }
.map { it.values.maxByOrNull { it.result } }
These are just a few examples, but I’m sure you can find lots of
great examples of collection processing in most bigger Kotlin
projects. The collection processing operations have expanded
the language capabilities such that Data Science, traditionally
the realm of Python, and competitive coding challenges are
very approachable and natural in Kotlin. Its usage is universal
and inter-domain, and I hope you will find the methods we
have covered in this chapter useful.
Sequences 157
Sequences
The readLines function returns a list with all the lines. If the
file is heavy, then this will be a heavy list. Allocating it in
memory is not only a cost but it also leads to the risk of
an OutOfMemoryError. A better option is to use useLines, which
reads and processes the file line by line. This solution will be
faster, and it’s safer for our memory:
What is a sequence?
You could say that the only formal difference between them
is the name. Both are Iterator types that allow the object to be
used with a “for” loop. Although Iterable and Sequence are as-
sociated with totally different behaviors (have different con-
tracts), nearly all their processing functions work differently.
Sequences are lazy, so intermediate functions for Sequence pro-
cessing (like filter or map) don’t do any calculations. Instead,
they return a new Sequence that decorates the previous one
with a new operation. All these computations are evaluated
during terminal operations like toList() or count(). On the
other hand, collection processing functions (those called on
Iterable) are eager: they immediately perform all operations
and return a new collection (typically a List).
Sequences 159
fun main() {
val seq = sequenceOf(1, 2, 3)
val filtered = seq.filter { print("f$it "); it % 2 == 1 }
println(filtered) // FilteringSequence@...
Order is important
fun main() {
listOf(1, 2, 3)
.filter { print("F$it, "); it % 2 == 1 }
.map { print("M$it, "); it * 2 }
.forEach { print("E$it, ") }
// Prints: F1, F2, F3, M1, M3, E2, E6,
}
The comparison between lazy processing (typical to Sequence) and eager pro-
cessing (typical to Iterable) in terms of operations order (the numbers next to
operations signalize in what order those operations are executed).
fun main() {
sequenceOf(1, 2, 3)
.filter { print("F$it, "); it % 2 == 1 }
.map { print("M$it, "); it * 2 }
.forEach { print("E$it, ") }
// Prints: F1, M1, E2, F2, F3, M3, E6,
}
fun main() {
for (e in listOf(1, 2, 3)) {
print("F$e, ")
if (e % 2 == 1) {
print("M$e, ")
val mapped = e * 2
print("E$mapped, ")
}
}
Sequences 163
fun main() {
val resI = (1..10).asIterable()
.map { print("M$it "); it * it }
.find { print("F$it "); it > 3 }
println(resI) // M1 M2 M3 M4 M5 M6 M7 M8 M9 M10 F1 F4 4
fun main() {
(1..10).asSequence()
.filter { print("F$it, "); it % 2 == 1 }
.map { print("M$it, "); it * 2 }
.find { it > 5 }
// Prints: F1, M1, F2, F3, M3,
(1..10)
.filter { print("F$it, "); it % 2 == 1 }
.map { print("M$it, "); it * 2 }
.find { it > 5 }
// Prints: F1, F2, F3, F4, F5, F6, F7, F8, F9, F10,
// M1, M3, M5, M7, M9,
}
fun main() {
val s = (1..6).asSequence()
.filter { print("F$it, "); it % 2 == 1 }
.map { print("M$it, "); it * 2 }
val l = (1..6)
.filter { print("F$it, "); it % 2 == 1 }
.map { print("M$it, "); it * 2 }
// F1, F2, F3, F4, F5, F6, M1, M3, M5,
fun main() {
generateSequence(1) { it + 1 }
.map { it * 2 }
.take(10)
.forEach { print("$it, ") }
// Prints: 2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
}
import java.math.BigInteger
prev = current
current += temp
}
}
fun main() {
print(fibonacci.take(10).toList())
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}
numbers
.filter { it % 10 == 0 } // 1 collection here
.map { it * 2 } // 1 collection here
.sum()
// In total, 2 collections created under the hood
numbers
.asSequence()
.filter { it % 10 == 0 }
.map { it * 2 }
.sum()
// No collections created
generateSequence(0) { it + 1 }.take(10).sorted().toList()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
generateSequence(0) { it + 1 }.sorted().take(10).toList()
// Infinite time. Does not return.
productsList.asSequence()
.filter { it.bought }
.map { it.price }
.sorted()
.take(10)
.sum()
productsList.asSequence()
.filter { it.bought }
.map { it.price }
.average()
productsList.stream()
.filter { it.bought }
.mapToDouble { it.price }
.average()
.orElse(0.0)
Java 8 streams are lazy and will be collected in the last (termi-
nal) processing step. There are three significant differences
between Java streams and Kotlin sequences:
Sequences 173
Summary
Sequences also have their own IDE debugger, which can help
us visualize how elements are processed. Sequences are not
designed to replace classic collection processing. You should
use them only when there’s a good reason, and you’ll be
rewarded with better performance and fewer memory prob-
lems.
Type Safe DSL Builders 176
// build.gradle
// Groovy
plugins {
id 'java'
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
implementation "org.jb.ktx:kotlinx-coroutines-core:1.6.0"
testImplementation "io.mockk:mockk:1.12.1"
testImplementation "org.junit.j:junit-jupiter-api:5.8.2"
testRuntimeOnly "org.junit.j:junit-jupiter-engine:5.8.2"
}
// build.gradle.kts
// Kotlin
plugins {
java
}
dependencies {
implementation(kotlin("stdlib"))
implementation("org.jb.ktx:kotlinx-coroutines-core:1.6.0")
testImplementation("io.mockk:mockk:1.12.1")
testImplementation("org.junit.j:junit-jupiter-api:5.8.2")
testRuntimeOnly("org.junit.j:junit-jupiter-engine:5.8.2")
}
In this chapter we will learn about the features that are used
in the above code. When we write this code, at every point we
can use concrete structures that were defined by the designer
of this configuration API. This is why it is called a Domain
Specific Language (DSL): creators define a small language
that is specifically designed to describe something concrete
using code, which in this case is a Gradle configuration.
Kotlin DSL is fully statically typed; so, at every point we are given suggestions of
what we can do, and if you make a typo, it is immediately marked.
// Kotlin
body {
div {
a("https://kotlinlang.org") {
target = ATarget.blank
+"Main site"
}
}
+"Some content"
}
// Kotlin
class HelloWorld : View() {
override val root = hbox {
label("Hello world") {
addClass(heading)
}
textfield {
promptText = "Enter your name"
}
}
}
DSLs are also used on the backend. For example, Ktor frame-
work API is based on Kotlin DSL. Thanks to that, endpoint
definitions are simple and readable but also flexible and con-
venient to use.
fun Routing.api() {
route("news") {
get {
val newsData = NewsUseCase.getAcceptedNews()
call.respond(newsData)
}
get("propositions") {
requireSecret()
val newsData = NewsUseCase.getPropositions()
call.respond(newsData)
}
}
Type Safe DSL Builders 181
// ...
}
As you can see, DSLs are already widespread, and there are
good reasons for this. They make it easy to define even com-
plex and hierarchical data structures. Inside these DSLs, we
can use everything that Kotlin offers, and we also have useful
hints. It is likely that you have already used some Kotlin DSLs,
but it is also important to know how to define them yourself.
Even if you don’t want to become a DSL creator, you’ll become
a better user.
fun main() {
println("A".myPlus1("B")) // AB
fun main() {
val myPlus2: String.(String) -> String =
fun String.(other: String) = this + other
println(myPlus2.invoke("A", "B")) // AB
println(myPlus2("A", "B")) // AB
println("A".myPlus2("B")) // AB
}
fun main() {
val myPlus3: String.(String) -> String = { other ->
this + other
// Inside, we can use receiver `this`,
// that is of type `String`
}
// Here, there is no receiver, so `this` has no meaning
println(myPlus3.invoke("A", "B")) // AB
println(myPlus3("A", "B")) // AB
println("A".myPlus3("B")) // AB
}
class Dialog {
var title: String = ""
var message: String = ""
var okButtonText: String = ""
var okButtonHandler: () -> Unit = {}
var cancelButtonText: String = ""
var cancelButtonHandler: () -> Unit = {}
fun show() {
/*...*/
}
}
fun main() {
val dialog = Dialog()
dialog.title = "Some dialog"
dialog.message = "Just accept it, ok?"
dialog.okButtonText = "OK"
dialog.okButtonHandler = { /*OK*/ }
dialog.cancelButtonText = "Cancel"
dialog.cancelButtonHandler = { /*Cancel*/ }
dialog.show()
}
fun main() {
val dialog = Dialog()
val init: Dialog.() -> Unit = {
title = "Some dialog"
message = "Just accept it, ok?"
okButtonText = "OK"
okButtonHandler = { /*OK*/ }
cancelButtonText = "Cancel"
cancelButtonHandler = { /*Cancel*/ }
}
init.invoke(dialog)
dialog.show()
}
The code above got a bit complicated, but we can extract the
repetitive parts into a function, like showDialog.
fun main() {
showDialog {
title = "Some dialog"
message = "Just accept it, ok?"
okButtonText = "OK"
okButtonHandler = { /*OK*/ }
cancelButtonText = "Cancel"
cancelButtonHandler = { /*Cancel*/ }
}
}
Using apply
fun main() {
Dialog().apply {
title = "Some dialog"
message = "Just accept it, ok?"
okButtonText = "OK"
okButtonHandler = { /*OK*/ }
cancelButtonText = "Cancel"
cancelButtonHandler = { /*Cancel*/ }
}.show()
}
Multi-level DSLs
Let’s say that our Dialog has been refactored, and there is now
a class that stores button properties:
class Dialog {
var title: String = ""
var message: String = ""
var okButton: Button? = null
var cancelButton: Button? = null
fun show() {
/*...*/
}
class Button {
var message: String = ""
var handler: () -> Unit = {}
}
}
fun main() {
showDialog {
title = "Some dialog"
message = "Just accept it, ok?"
okButton = Dialog.Button()
okButton?.message = "OK"
okButton?.handler = { /*OK*/ }
cancelButton = Dialog.Button()
cancelButton?.message = "Cancel"
Type Safe DSL Builders 189
cancelButton?.handler = { /*Cancel*/ }
}
}
fun main() {
showDialog {
title = "Some dialog"
message = "Just accept it, ok?"
okButton = makeButton {
message = "OK"
handler = { /*OK*/ }
}
cancelButton = makeButton {
message = "Cancel"
handler = { /*Cancel*/ }
}
}
}
This is better, but it’s still not perfect. The user of our DSL
needs to know that there is a makeButton function that is used
to create a button. In general, we prefer to require users
to remember as little as possible. Instead, we could make
okButton and cancelButton methods inside Dialog to create but-
tons. Such functions are easily discoverable and their usage is
really readable.
Type Safe DSL Builders 190
class Dialog {
var title: String = ""
var message: String = ""
private var okButton: Button? = null
private var cancelButton: Button? = null
fun show() {
/*...*/
}
class Button {
var message: String = ""
var handler: () -> Unit = {}
}
}
fun main() {
showDialog {
title = "Some dialog"
message = "Just accept it, ok?"
okButton {
message = "OK"
handler = { /*OK*/ }
}
cancelButton {
message = "Cancel"
handler = { /*Cancel*/ }
Type Safe DSL Builders 191
}
}
}
DslMarker
Our DSL builder for defining dialogs has one safety concern
that we need to fix: by default, you can implicitly access
elements from the outer receiver. In our example, this means
that we can accidentally set the dialog title inside of okButton.
fun main() {
showDialog {
title = "Some dialog"
message = "Just accept it, ok?"
okButton {
title = "OK" // This sets the dialog title!
handler = { /*OK*/ }
}
cancelButton {
message = "Cancel"
handler = { /*Cancel*/ }
}
}
}
@DslMarker
annotation class DialogDsl
@DialogDsl
class Dialog {
var title: String = ""
var message: String = ""
private var okButton: Button? = null
private var cancelButton: Button? = null
@DialogDsl
@DialogDsl
fun cancelButton(init: Button.() -> Unit) {
cancelButton = Button().apply(init)
}
fun show() {
/*...*/
}
@DialogDsl
class Button {
var message: String = ""
var handler: () -> Unit = {}
}
}
@DialogDsl
fun showDialog(init: Dialog.() -> Unit) {
Dialog().apply(init).show()
}
Type Safe DSL Builders 194
As you might notice in the above image, DSL calls now have
a different color (to me, it looks like burgundy). This color
should be the same no matter which computer I start this code
with. At the same time, DSLs can have one of four different
colors that are specified in IntelliJ, and the style is chosen
based on the hash of the DSL’s annotation name. So, if you re-
name DialogDsl to something else, you will most likely change
the color of this DSL function call.
Type Safe DSL Builders 195
The four possible styles for DSL elements can be customized in IntelliJ IDEA.
head and body only make sense inside html, so they need to be
called on its receiver. We will define them inside HtmlBuilder.
Since they have children, they will have receivers: HeadBuilder
and (my favorite) BodyBuilder.
class HtmlBuilder {
fun head(init: HeadBuilder.() -> Unit) {
/*...*/
}
class HeadBuilder {
var title: String = ""
class BodyBuilder {
fun h1(text: String) {
/*...*/
}
functions are still empty. We need them to store all the values
somewhere. For the sake of simplicity, I will store everything
in the builder we just defined.
In HeadBuilder, I just need to store the defined styles. We will
use a list.
class HeadBuilder {
var title: String = ""
private var styles: List<String> = emptyList()
class BodyBuilder {
private var elements: List<BodyElement> = emptyList()
class HtmlBuilder {
private var head: HeadBuilder? = null
private var body: BodyBuilder? = null
Now our builders collect all the data defined in the DSL. We
can just parse it and make HTML text. Here is a complete
example in which the DslMarker and toString functions present
our HTML as text.
// DSL definition
@DslMarker
annotation class HtmlDsl
@HtmlDsl
fun html(init: HtmlBuilder.() -> Unit): HtmlBuilder {
return HtmlBuilder().apply(init)
}
@HtmlDsl
class HtmlBuilder {
private var head: HeadBuilder? = null
private var body: BodyBuilder? = null
@HtmlDsl
fun head(init: HeadBuilder.() -> Unit) {
this.head = HeadBuilder().apply(init)
}
@HtmlDsl
fun body(init: BodyBuilder.() -> Unit) {
this.body = BodyBuilder().apply(init)
}
@HtmlDsl
class HeadBuilder {
var title: String = ""
private var cssList: List<String> = emptyList()
@HtmlDsl
fun css(body: String) {
cssList += body
}
@HtmlDsl
class BodyBuilder {
private var elements: List<BodyElement> = emptyList()
@HtmlDsl
fun h1(text: String) {
this.elements += H1(text)
}
@HtmlDsl
fun h3(text: String) {
this.elements += H3(text)
}
}
}
// DSL usage
val html = html {
head {
title = "My website"
css("Some CSS1")
css("Some CSS2")
}
body {
h1("Title")
h3("Subtitle 1")
+"Some text 1"
h3("Subtitle 2")
+"Some text 2"
}
}
fun main() {
println(html)
}
/*
<html>
<head>
<title>My website</title>
Type Safe DSL Builders 203
<style>Some CSS1</style>
<style>Some CSS2</style>
</head>
<body>
<h1>Title</h1>
<h3>Subtitle 1</h3>
Some text 1
<h3>Subtitle 2</h3>
Some text 2
</body>
</html>
*/
Summary
Scope functions
let
fun main() {
println(listOf("a", "b", "c").map { it.uppercase() })
// [A, B, C]
println("a".let { it.uppercase() }) // A
}
class CoursesService(
private val userRepository: UserRepository,
private val coursesRepository: CoursesRepository,
private val userCoursesFactory: UserCoursesFactory,
) {
// Imperative approach, without let
fun getActiveCourses(token: String): UserCourses? {
val user = userRepository.getUser(token)
?: return null
val activeCourses = coursesRepository
.getActiveCourses(user.id) ?: return null
return userCoursesFactory.produce(activeCourses)
}
class UserCreationRequest(
val id: String,
val name: String,
val surname: String,
)
class UserDto(
val userId: String,
val firstName: String,
val lastName: String,
)
class UserCreationRequest(
val name: String,
val surname: String,
)
class UserDto(
val userId: String,
val firstName: String,
val lastName: String,
)
class UserCreationService(
private val userRepository: UserRepository,
// Anti-pattern!
private fun UserCreationRequest.toUserDto() = UserDto(
userId = idGenerator.generate(),
firstName = this.name,
lastName = this.surname,
)
}
class UserCreationRequest(
val name: String,
val surname: String,
)
class UserDto(
val userId: String,
val firstName: String,
val lastName: String,
)
class UserCreationService(
private val userRepository: UserRepository,
private val idGenerator: IdGenerator,
) {
fun addUser(request: UserCreationRequest): User =
request.let { createUserDto(it) }
// or request.let(::createUserDto)
.also { userRepository.addUser(it) }
.toUser()
Scope functions 210
class UserCreationService(
private val userRepository: UserRepository,
private val userDtoFactory: UserDtoFactory,
) {
fun addUser(request: UserCreationRequest): User =
request.let { userDtoFactory.produce(it) }
.also { userRepository.addUser(it) }
.toUser()
// or
// fun addUser(request: UserCreationRequest): User =
// request.let(userDtoFactory::produce)
// .also(userRepository::addUser)
// .toUser()
}
The second typical use case for let is when we want to move
an operation to the end of processing. Let’s get back to our
example, where we were reading an object from a zip file,
but this time we will assume that we need to do something
with that object in the end. For simplification, we might
be printing it. Again, we face the same problem: we either
need to introduce a variable or wrap the processing with a
misplaced print call.
Scope functions 211
// Terrible
print(
FileInputStream("/someFile.gz")
.let(::BufferedInputStream)
.let(::ZipInputStream)
.let(::ObjectInputStream)
.readObject()
)
FileInputStream("/someFile.gz")
.let(::BufferedInputStream)
.let(::ZipInputStream)
.let(::ObjectInputStream)
.readObject()
.let(::print)
FileInputStream("/someFile.gz")
.let(::BufferedInputStream)
.let(::ZipInputStream)
.let(::ObjectInputStream)
.readObject()
?.let(::print)
fun showUserNameIfPresent() {
// will not work, because cannot smart-cast a property
// if (user != null) {
// println(user.name)
// }
// works
// val u = user
// if (u != null) {
// println(u.name)
// }
// perfect
user?.let { println(it.name) }
}
Scope functions 213
These are the key cases where let is used. As you can see,
it is pretty useful but there are other scope functions with
similar characteristics. Let’s see these, starting from the one
mentioned a few times already: also.
also
class CachingDatabaseFactory(
private val databaseFactory: DatabaseFactory,
) : DatabaseFactory {
private var cache: Database? = null
fun showUserNameIfPresent() {
user?.also { println(it.name) }
}
fun readAndPrint() {
FileInputStream("/someFile.gz")
.let(::BufferedInputStream)
Scope functions 215
.let(::ZipInputStream)
.let(::ObjectInputStream)
.readObject()
?.also(::print)
}
class UserCreationService(
private val userRepository: UserRepository,
) {
fun readUser(token: String): User? =
userRepository.findUser(token)
.takeIf { it.isValid() }
?.toUser()
}
apply
fun main() {
val node = Node("parent")
node.makeChild("child")
}
fun main() {
val node = Node("parent")
node.makeChild("child") // Created child
}
with
// unit-test assertions
with(user) {
assertEquals(aName, name)
assertEquals(aSurname, surname)
assertEquals(aWebsite, socialMedia?.websiteUrl)
assertEquals(aTwitter, socialMedia?.twitter)
assertEquals(aLinkedIn, socialMedia?.linkedin)
assertEquals(aGithub, socialMedia?.github)
}
run
Context receivers
fun main() {
println("alex".capitalize()) // Alex
println("this is text".capitalize()) // This is text
println((1..5).product()) // 120
println(listOf(1, 3, 5).product()) // 15
}
The second kind of use case is less obvious but also quite
common. We turn functions into extensions to explicitly pass
a context of their use. Let’s take a look at a few examples.
Consider a situation in which you use Kotlin HTML DSL, and
you want to extract some structures into a function. We might
use the DSL we defined in the Type Safe DSL Builders chapter
and define a standardHead function that sets up a standard
head. Such a function needs a reference to HtmlBuilder, which
we might provide as an extension receiver.
Context receivers 223
fun HtmlBuilder.standardHead() {
head {
title = "My website"
css("Some CSS1")
css("Some CSS2")
}
}
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
interface LoggingContext {
val logger: Logger
}
fun LoggingContext.sendNotification(
notification: NotificationData
) {
logger.info("Sending notification $notification")
notificationSender.send(notification)
}
// Do
html {
standardHead()
}
// Don't
builder.standardHead()
// Will do
with(receiver) {
standardHead()
}
class Foo {
fun foo() {
print("Foo")
}
}
context(Foo)
fun callFoo() {
foo()
}
fun main() {
with(Foo()) {
callFoo()
}
}
fun main() {
Foo().callFoo() // ERROR
}
context(Foo)
fun callFoo() {
[email protected]() // OK
this.foo() // ERROR, this is not defined
}
class Foo {
fun foo() {
print("Foo")
}
}
class Boo {
fun boo() {
println("Boo")
}
}
context(Foo, Boo)
fun callFooBoo() {
foo()
boo()
}
context(Foo, Boo)
fun callFooBoo2() {
callFooBoo()
}
fun main() {
with(Foo()) {
with(Boo()) {
callFooBoo() // FooBoo
callFooBoo2() // FooBoo
}
Context receivers 228
}
with(Boo()) {
with(Foo()) {
callFooBoo() // FooBoo
callFooBoo2() // FooBoo
}
}
}
package fgfds
interface Foo {
fun foo() {
print("Foo")
}
}
interface Boo {
fun boo() {
println("Boo")
}
}
context(Foo, Boo)
fun callFooBoo() {
foo()
boo()
}
}
}
fun main() {
val fooBoo = FooBoo()
fooBoo.call() // FooBoo
}
Use cases
context(HtmlBuilder)
fun standardHead() {
head {
title = "My website"
css("Some CSS1")
css("Some CSS2")
}
}
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
context(CoroutineScope)
fun <T> Flow<T>.launchFlow(): Job =
[email protected] { collect() }
context(LoggingContext)
fun sendNotification(notification: NotificationData) {
logger.info("Sending notification $notification")
notificationSender.send(notification)
}
context(ChristmasLetterBuilder)
operator fun String.unaryPlus() {
addItem(this)
}
context(View)
val Float.dp get() = this * resources.displayMetrics.density
context(View)
val Int.dp get() = this.toFloat().dp
As you can see, there are many cases in which context receiver
functionality is useful, but remember that most of them are
related to DSL builders.
package sdfgv
class ApplicationConfig(
val name: String,
) {
fun start() {
print("Start application")
}
}
context(ApplicationConfig)
class ApplicationControl(
val applicationName: String = [email protected]
) {
fun start() {
print("Using control: ")
[email protected]()
}
}
fun main() {
with(ApplicationConfig("AppName")) {
val control = ApplicationControl()
println(control.applicationName) // AppName
control.start() // Using control: Start application
}
}
Concerns
// Don't do this
context(
LoggerContext,
NotificationSenderProvider, // not a context
NotificatonsRepository // not a context
) // it might hard to call such a function
suspend fun sendNotifications() {
log("Sending notifications")
val notifications = getUnsentNotifications() // unclear
val sender = create() // unclear
for (n in notifications) {
sender.send(n)
}
log("Notifications sent")
}
class NotificationsController(
notificationSenderProvider: NotificationSenderProvider,
notificationsRepository: NotificationsRepository
) : Logger() {
@Post("send")
suspend fun send() {
with(notificationSenderProvider) { // avoid such calls
with(notificationsRepository) { //avoid such calls
sendNotifications()
}
}
}
}
Context receivers 234
// Don't do that
context(LoggerContext)
suspend fun sendNotifications(
notificationSenderProvider: NotificationSenderProvider,
notificationsRepository: NotificationsRepository
) {
log("Sending notifications")
val notifications = notificationsRepository
.getUnsentNotifications()
val sender = notificationSenderProvider.create()
for (n in notifications) {
sender.send(n)
}
log("Notifications sent")
}
class NotificationsController(
notificationSenderProvider: NotificationSenderProvider,
notificationsRepository: NotificationsRepository
) : Logger() {
@Post("send")
suspend fun send() {
sendNotifications(
notificationSenderProvider,
notificationsRepository
)
}
}
Context receivers 235
Summary
Context receivers solve all these problems and are very conve-
nient. I’m looking forward to them becoming a stable feature
that I can use in my projects.
[12_1]: More about this in the Kotlin Coroutines: Deep Dive
book.
[12_2]: See Effective Kotlin, Item 15: Consider referencing re-
ceivers explicitly.
A birds-eye view of Arrow 236
At the time of writing, the library is in the 1.1.x series, with 2.0
being planned and worked on.
Being a library that targets functional programming, Arrow
Core includes several extensions for function types, in the
arrow.core package. The first one is compose, which creates a
new function by executing two functions, one after another:
people.filter(
Boolean::not compose ::goodString compose Person::name
)
// instead of
people.filter { !goodString(it.name) }
(String::isPrefixOf).partially1("FP")
Memoization
In this case, by the time we get to the second call to fib(2), the
entire sequence at that point has been computed and cached.
We go from an exponential blowup to a linear function.
Now you can use this generator to test the behavior of func-
tions like map.
Error Handling
Let’s take a simple example that reads a value from the envi-
ronment and results in an optional String value. In the func-
tion below, the exception from System.getenv is swallowed and
flattened into null.
A birds-eye view of Arrow 241
Now we can use this function to read values from our envi-
ronment and build a simple example of combining nullable
functions to load a data class Config(val port: Int). Within
Java, the most common way to deal with null is to use if(x !=
null), so let’s explore that first.
import arrow.core.continuations.nullable
return Result.failure(IllegalArgumentException("Required
port value was null.")) when encountering null.
import arrow.core.continuations.result
It doesn’t make sense to use Result for every error type you
want to model. With Either, we can model the different fail-
ures to load the configuration in a more expressive and better-
typed way without depending on exceptions or Result.
A birds-eye view of Arrow 245
Before we dive into solving our problem with Either, let’s first
take a quick look at the Either type itself.
Either<E, A> models the result of a computation that might
fail with an error of type E or success of type A. It’s a sealed
class, and the Left and Right subtypes accordingly represent
the Error and Success cases.
When modeling our errors with Either, we can use any type to
represent failures arising from loading our Config.
In our Result example, we used the following exceptions to
model our errors:
import arrow.core.continuations.either
import arrow.core.continuations.either
@optics
data class Address(
val zipcode: String,
val country: String
) {
companion object
}
@optics
data class Person(
val name: String,
val age: Int,
val address: Address
) {
companion object
}
@optics
data class BirthdayResult(
val day: LocalDate,
val people: List<Person>
) {
companion object
}
How do we change the age field for all of them? We not only
need nested copy methods; we must also be careful that people
is a list, so transformation occurs using map.
A birds-eye view of Arrow 252