data class UserInput(val raw: String)
data class ParsedInput(val value: String)
data class ValidatedInput(val value: String)
data class FormattedOutput(val value: String)
fun processInput(input: UserInput): FormattedOutput {
// Parse
val parsed = if (input.raw.isNotBlank()) {
ParsedInput(input.raw.trim())
} else {
throw IllegalArgumentException("Input must not be blank")
}
// Validate
val validated = if (parsed.value.length >= 3) {
ValidatedInput(parsed.value.lowercase())
} else {
throw IllegalArgumentException("Input too short")
}
// Format
return FormattedOutput("Hello, ${validated.value.replaceFirstChar(Char::titlecase)}!")
}
val parse: (String) -> String = { raw ->
require(raw.isNotBlank()) { "Input must not be blank" }
raw.trim()
}
val validate: (String) -> String = { value ->
require(value.length >= 3) { "Input too short" }
value.lowercase()
}
val format: (String) -> String = {
"Hello, ${it.replaceFirstChar(Char::titlecase)}!"
}
// Usage
val result = " alice "
.let(parse)
.let(validate)
.let(format) // result: "Hello, Alice!"
first-class
functional types to represent functionslambda expressions(A, B) -> C denotes the function with two arguments of type A and B Nullable function type is defined by surrounding function type with a parentheses, followed by question mark (?)
receiver type A is a receiver typeUnit as a return value typeFunctional type can be combined using parentheses like in the example above
Important: arrow notation is a right associative which means that
is equivalent to previous example, but not to
In order to obtain instance of a functional types we can use one of the following approaches:
Lambda expression represents the block of code that we can assign to variable, or pass as an argument of the other function
val sum = { x: Int, y: Int -> x + y }
always surrounded by curly bracesinside curly braces and have optional type annotations.-> signinferred, the last (or possibly single) expression inside the lambda body is treated as the return valueval sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
What will this print?
println(sum)
(kotlin.Int, kotlin.Int) -> kotlin.Int
Program prints a type of a variable.
In order to get value of a sum, function type has to be invoked
Higher-order function is a function that takes other function as an argument
fun List<Int>.filter(predicate: (Int) -> Boolean): List<Int> {
val numbers = ArrayList<Int>()
for (num in this) {
if (predicate(num)) numbers.add(num)
}
return numbers
}
fun List<Int>.filter(predicate: (Int) -> Boolean): List<Int> {
// implementation
}
val isOdd: (Int) -> Boolean = { x: Int -> x % 2 != 0 }
val oddNumbers = listOf(1, 2, 3).filter(isOdd) // returns [1, 3]
val oddNumbers = listOf(1, 2, 3)
.filter({ x: Int -> x % 2 != 0 }) // returns [1, 3]
val oddNumbers = listOf(1, 2, 3)
.filter({ x: Int -> x % 2 != 0 }) // returns [1, 3]
val oddNumbers = listOf(1, 2, 3)
.filter() { x: Int -> x % 2 != 0 } // returns [1, 3]
last parameter of a function is a function outside the parenthesestrailing lambdaval oddNumbers = listOf(1, 2, 3)
.filter() { x: Int -> x % 2 != 0 } // returns [1, 3]
If the lambda is the only argument, the parentheses can be omitted entirely
val oddNumbers = listOf(1, 2, 3)
.filter { x: Int -> x % 2 != 0 } // returns [1, 3]
itit = implicit parameter nameval oddNumbers = listOf(1, 2, 3)
.filter { it % 2 != 0 } // returns [1, 3]
listOf(1, 2, 3)
.filter({ x: Int -> x % 2 != 0 }) // returns [1, 3]
listOf(1, 2, 3)
.filter() { x: Int -> x % 2 != 0 } // returns [1, 3]
listOf(1, 2, 3)
.filter { x: Int -> x % 2 != 0 } // returns [1, 3]
listOf(1, 2, 3)
.filter { it % 2 != 0 } // returns [1, 3]
fun doSomething(func1: (String) -> Unit = {}, func2: (String) -> Unit = {}) {
func1(“one”)
func2(“two”)
}
doSomething() { text -> println(text) }
doSomething({ text -> println(text) })
What is the output of the first and what of the second invocation?
fun doSomething(func1: (String) -> Unit = {}, func2: (String) -> Unit = {}) {
func1(“one”)
func2(“two”)
}
doSomething { text -> println(text) }
prints two
because this is a trailing lambda
doSomething({ text -> println(text) })
prints one
because order of arguments is taken into account and func1 is passed here
Often in the context of FP it is necessary to operate with the following functions: map, filter, and fold
map allows us to perform a function over each element in a collection:
val list = listOf(1, 2, 3)
list.map { it * it } // [1, 4, 9]
What is the main difference between map and forEach?
Function fold creates a mutable accumulator, which is updated on each round of the for and returns one value
val l = listOf(1, 2, 3)
list.fold(0) { acc, x -> acc + x } // 6
You can implement the fold function for any type
For example, you can fold a tree into a string representation
There are also right and left folds.
They are equivalent if the operation is associative: (a ○ b) ○ c = a ○ (b ○ c),
but in any other case they yield different results.
val list = listOf(1, 2, 3)
list.fold(0) { acc, x -> acc + x } // (((0 + 1) + 2) + 3) = 6
list.foldRight(0) { x, acc -> acc + x } // (1 + (2 + (3 + 0))) = 6
"PWND".fold("") { acc, x -> "${acc}${acc}$x" }
// PPWPPWNPPWPPWND
"PWND".foldRight("") { x, acc -> "${acc}${acc}$x" }
// DDNDDNWDDNDDNWP
Be mindful with the order of your lambda arguments:
list.fold(0) { acc, x -> acc - x } // (((0 - 1) - 2) - 3) = -6
list.foldRight(0) { x, acc -> acc - x } // (-1 + (-2 + (0 - 3))) = -6
list.foldRight(0) { acc, x -> acc - x } // (1 - (2 - (3 - 0))) = 2
val string = """
One-one was a race horse.
Two-two was one too.
One-one won one race.
Two-two won one too.
""".trimIndent()
val result = string
.split(" ", "-", ".", System.lineSeparator())
.filter { it.isNotEmpty() }
.map { it.lowercase() }
.groupingBy { it }
.eachCount()
.toList()
.sortedBy { (_, count) -> count }
.reversed()
val string = """
One-one was a race horse.
Two-two was one too.
One-one won one race.
Two-two won one too.
""".trimIndent()
val result = string
.split(" ", "-", ".", System.lineSeparator())
[One, one, was, a, race, horse, , Two, two, was, one, too, , One, one, won, one, race, , Two, two, won, one, too, , ]
[One, one, was, a, race, horse, , Two, two, was, one, too, , One, one, won, one, race, , Two, two, won, one, too, , ]
val result = string
.split(" ", "-", ".", System.lineSeparator())
.filter { it.isNotEmpty() } // <-- next action
[One, one, was, a, race, horse, Two, two, was, one, too, One, one, won, one, race, Two, two, won, one, too]
[One, one, was, a, race, horse, Two, two, was, one, too, One, one, won, one, race, Two, two, won, one, too]
val result = string
.split(" ", "-", ".", System.lineSeparator())
.filter { it.isNotEmpty() }
.map { it.lowercase() } // <-- next action
[one, one, was, a, race, horse, two, two, was, one, too, one, one, won, one, race, two, two, won, one, too]
[one, one, was, a, race, horse, two, two, was, one, too, one, one, won, one, race, two, two, won, one, too]
val result = string
.split(" ", "-", ".", System.lineSeparator())
.filter { it.isNotEmpty() }
.map { it.lowercase() }
.groupingBy { it } // <-- next action
.eachCount() // <-- next action
or
val result = string
.split(" ", "-", ".", System.lineSeparator())
.filter { it.isNotEmpty() }
.groupBy({ it.lowercase() }, { it }) //*
.mapValues { (key, value) -> value.size } //*
{one=7, was=2, a=1, race=2, horse=1, two=4, too=2, won=2}
{one=7, was=2, a=1, race=2, horse=1, two=4, too=2, won=2}
val result = string
.split(" ", "-", ".", System.lineSeparator())
.filter { it.isNotEmpty() }
.map { it.lowercase() }
.groupingBy { it }
.eachCount()
.toList() // <-- next action
[(one, 7), (was, 2), (a, 1), (race, 2), (horse, 1), (two, 4), (too, 2), (won, 2)]
[(one, 7), (was, 2), (a, 1), (race, 2), (horse, 1), (two, 4), (too, 2), (won, 2)]
val result = string
.split(" ", "-", ".", System.lineSeparator())
.filter { it.isNotEmpty() }
.map { it.lowercase() }
.groupingBy { it }
.eachCount()
.toList()
.sortedBy { (_, count) -> count } // <-- next action
[(a, 1), (horse, 1), (was, 2), (race, 2), (too, 2), (won, 2), (two, 4), (one, 7)]
[(a, 1), (horse, 1), (was, 2), (race, 2), (too, 2), (won, 2), (two, 4), (one, 7)]
val result = string
.split(" ", "-", ".", System.lineSeparator())
.filter { it.isNotEmpty() }
.map { it.lowercase() }
.groupingBy { it }
.eachCount()
.toList()
.sortedBy { (_, count) -> count }
.reversed() // <-- next action
[(one, 7), (two, 4), (won, 2), (too, 2), (race, 2), (was, 2), (horse, 1), (a, 1)]
[(a, 1), (horse, 1), (was, 2), (race, 2), (too, 2), (won, 2), (two, 4), (one, 7)]
val result = string
// ...
.toList()
.sortedBy { (_, count) -> count }
.reversed()
val result = string
// ...
.toList()
.sortedWith { l, r -> r.second - l.second }
OR
val result = string
// ...
.toList()
.sortedByDescending { (_, count) -> count }
[(one, 7), (two, 4), (won, 2), (too, 2), (race, 2), (was, 2), (horse, 1), (a, 1)]
fun <F> withFunction(number: Int, even: F, odd: F): F =
when (number % 2) {
0 -> even
else -> odd
}
withFunction(4, println("even"), println("odd"))
What will be printed to the console?
even
odd
Arguments of function are be evaluated before its body is executed (eager execution)
fun <F> withLambda(number: Int, even: () -> F, odd: () -> F): F =
when (number % 2) {
0 -> even()
else -> odd()
}
withLambda(4, { println("even") }, { println("odd") })
What will be printed to the console?
even
Arguments of function are be evaluated when they are invoked (lazy execution)
Using higher order functions, such as a filter function shown below imposes runtime performance penalties, because every function type parameter is an object, which implies memory allocation
fun List<Int>.filter(predicate: (Int) -> Boolean): List<Int> {
val numbers = ArrayList<Int>()
for (num in numbers) {
if (predicate(num)) numbers.add(num)
}
return numbers
}
This kind of an overhead can be eliminated by inlining the lambda expression, which means that content of the function + it’s function parameter is inlined in the call site.
Higher order functions can be inlined by stating the inline modifier at the beginning of the function definition
inline fun List<Int>.filter(predicate: (Int) -> Boolean): List<Int> {
val numbers = ArrayList<Int>()
for (num in numbers) {
if (predicate(num)) numbers.add(num)
}
return numbers
}
Inlined
inline fun List<Int>.filter(predicate: (Int) -> Boolean): List<Int> {
val numbers = ArrayList<Int>()
for (num in numbers) {
if (predicate(num)) numbers.add(num)
}
return numbers
}
listOf(1, 2, 3).filter { it % 2 == 0 }
Bytecode: no allocation
val numbers = ArrayList<Int>()
for (num in listOf(1, 2, 3)) {
if (num % 2 == 0) numbers.add(num)
}
Non-Inlined
fun List<Int>.filter(predicate: (Int) -> Boolean): List<Int> {
val numbers = ArrayList<Int>()
for (num in numbers) {
if (predicate(num)) numbers.add(num)
}
return numbers
}
listOf(1, 2, 3).filter { it % 2 == 0 }
Bytecode - object allocation
listOf(1, 2, 3).filter(object: Function1<Int, Boolean> {
operator fun invoke(num: Int): Boolean
= it % 2 == 0
})
| Function | Object reference | Return value | Is extension function |
|---|---|---|---|
let |
it |
Lambda result | Yes |
run |
this |
Lambda result | Yes |
run |
- | Lambda result | No: called without the context object |
with |
this |
Lambda result | No: takes the context object as an argument |
apply |
this |
Context object | Yes |
also |
it |
Context object | Yes |
Allows to check the argument for being non-null, or mapping one object to another
data class User(val fullName: String)
fun getUserOrNull(): User? {…}
val user = getUserOrNull()
if (user != null) {
println(user)
}
user?.let { println(it) }
Context object is available as an argument it
Return value is the lambda result
inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}
Runs block of code in the context of the receiver (receiver can be nullable)
val numbers = mutableListOf("one", "two", "three")
val countOnes = numbers.run {
// We are referencing numbers list as `this`
// `this` can be omitted
add("four")
add("five")
count { it == “one” }
}
Context object is available as a receiver this
Return value is the lambda result
inline fun <T, R> T.run(block: T.() -> R): R {
return this.block()
}
Can be used in combination with an Elvis operator ?: as a guard
val nullableNumbers: List<Int>? = listOf(1, 2, 3)
val number = nullableNumbers ?: run {
logger.log("Number is not initialized.")
return
}
let + regular run != if else statement
val nullableInt: Int? = 1
nullableInt?.let {
maybeNullString(it)
} ?: run {
logger.log("Int is not initialized.")
return
}
fun maybeNullString(a: Int): String?
val nullableInt: Int? = 1
if(nullableInt != null) {
maybeNullString(it)
} else {
logger.log("Number is not initialized.")
return
}
fun maybeNullString(a: Int): String?
On the left side, both the let and run functions can be executed
if nullableInt has not-null value, and maybeNullString method returns null
The return value is the result of the argument lambda function
inline fun <R> run(block: () -> R): R {
return block()
}
Runs block of a code in the context of a non-null receiver, used as a regular function
val pair = 1 to 2
with(pair) {
val left = this.first
val second = this.second
println("$first $second")
}
Context object is passed as an argument of with function
but inside block lambda it is available as a receiver this
The return value is lambda result
inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
The common case for using an apply is an object configuration / initialization
val user = User()
user.firstName = "Marko"
user.lastName = "Markovic"
val user = User().apply {
firstName = "Marko"
lastName = "Markovic"
}
The context object is available as a receiver this
The return value of the function is object itself.
inline fun <T> T.apply(block: T.() -> Unit): T {
this.block()
return this
}
Used to apply side effect in a chain of operations
val numbers = listOf(1, 2, 3, 4, 5, 6)
numbers
.map { it*it }
.also { println("Squared numbers are $it") }
.filter { it % 3 == 0 }
Context object is available as an argument it
The return value of the function is object itself
inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}
FP in Kotlin does not kill OOP
Each of the concepts brings its own advantages and disadvantages
It is important to combine them to get concise, readable and understandable code!
If you are interested in FP in Kotlin: https://arrow-kt.io/
Thanx!