Kotlin programming language
Kotlin programming language
Functional Programming

Kotlin programming language

What is Functional Programming?

Kotlin

Kotlin programming language

Our approach

FP, like other concepts, has its advantages and disadvantages

We will focus on its strengths


Disclaimer: There won’t be any deep math, Lisp or Haskell examples in this lecture.
We will look at what we consider to be the most important FP features
that can be used in Kotlin

Kotlin programming language

What is it?

Besides being the object-oriented programming language (OOP), Kotlin also borrows concepts from functional programming (FP)

FP is a programming paradigm where programs are constructed by
applying and composing functions

Imperative approach

var sum = 0
for (item in list) {
    if (item > 0) {
        sum += item * item
    }
}

VS

Declarative approach

list.filter{ it > 0 }
    .map{ it * it }
    .sum()
Kotlin programming language

Key concepts

  • Immutability
  • Pure functions
  • Function composition
Kotlin programming language

Mutability

class User(var name: String, var email: String)

fun main() {
    val user = User("Alice", "alice@example.com")
    sendWelcomeEmail(user)
}
fun sendWelcomeEmail(user: User) {
    // Surprise! This function silently mutates the object it received
    user.email = "welcome-sent-to-${user.email}"
}
Kotlin programming language

Mutability #2

  • With mutable objects, you can never trust a reference
  • Reasoning is non-local
  • Sharing objects is not safe nor predictable
  • Concurrency is difficult to reason about and very hard to implement correctly
  • Mutation is destroying information
Kotlin programming language

Immutability

  • Immutable object is an object whose state can’t be modified after it is created
  • Use data class with val properties to create immutable objects in Kotlin
  • To operate on a “modified state” the new object with a modified value should be created every time
data class User(val name: String, val age: Int, val email: String)

val original = User("Alice", 30)

// Update age while preserving other properties
val olderAlice = original.copy(age = 31)

// Update multiple properties at once
val rebranded = original.copy(name = "Alice Smith", email = "alice.smith@example.com")
Kotlin programming language

Pure functions

Pure function:

  1. deterministic - always returns a same result for a same input
  2. without side effects (interactions with external world)

Some examples of side effects are:

  • Mutating variables outside of the function scope
  • Writing to file system, updating database, printing to console, sending email

An example - isEven always returns the same result for the same input, no side effects

fun isEven(x: Int): Boolean = x % 2 == 0
Kotlin programming language

Is this function pure?

fun divide(a: Int, b: Int): Int {
    if (b == 0) throw ArithmeticException("Division by zero")
    return a / b
}

No, because throwing exception is a side effect

  • function signature lies - it actually returns Int | ArithmeticException
  • affects control flow (i.e. interacts with external world)
Kotlin programming language

Side Effects: Why so bad?

  • Predictability
  • Reasoning
  • Testing
  • Reusability
  • Concurrency
  • Composition
  • Refactoring
  • Debugging
  • Dishonesty
  • Cognitive load
  • Portability
  • Idempotency
Kotlin programming language

Functions in Math vs Functions in Programming

Math functions

  • Deterministic: Always return the same result for the same input
  • No concept of state or memory
  • Cannot have side effects
  • Defined for all values in their domain
  • Pure = no side effects + deterministic
  • Referential Transparency = replace expression with its computed value

Programming functions

  • Nonedeterministic: may return different results each call
  • Can read and modify global state (i.e. not isolated)
  • Can perform side effects
  • May throw exceptions or crash
  • Can lack referential transparency
Kotlin programming language

What is function composition?

Combining two funcs to produce a new one

If f : B → C and g : A → B
then (f ∘ g)(x) = f(g(x))

Output of g becomes input of f.

x g(x) f(g(x)) g f
Input → transform → transform

Why it matters

Reusability — write small functions, combine them freely
Readability — pipelines read like prose: parse → validate → format
Testability — test each step in isolation

Kotlin programming language

Classic approach

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)}!")
}
Kotlin programming language

Functional composition approach

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!"
Kotlin programming language

Benefits of FP

  • Easier debugging and testing
  • Predictability
  • Reusability and modularity
  • Expressiveness
Kotlin programming language

Function Types

Kotlin

Kotlin programming language

Function Types

  • Functions in Kotlin are first-class
    • can be stored in variable or data structures
    • passed to other functions as an argument
  • To facilitate this Kotlin defines:
    • functional types to represent functions
    • lambda expressions
    • anonymous functions
Kotlin programming language

Functional types

(A, B) C

  • special notation that corresponds to the signatures of the functions, their parameters and return values
  • all functional types have parenthesized list of parameter types and a return type
  • (A, B) -> C denotes the function with two arguments of type A and B
    and return value of type C
Kotlin programming language

Functional types #2

((A, B) C)?

Nullable function type is defined by surrounding function type with a parentheses, followed by question mark (?)

Kotlin programming language

Functional types #3

A.(B) C

  • Functional type can optionally have an additional receiver type
    specified before the dot (.) in the notation
  • In this example, A is a receiver type
Kotlin programming language

Functional types #4

() Unit

  • List of function type parameters can be empty
  • Functional type without return value has to specify Unit as a return value type
Kotlin programming language

Functional types #5

(Int) ((Int) Int)

Functional type can be combined using parentheses like in the example above

Important: arrow notation is a right associative which means that

(Int) (Int) Int

is equivalent to previous example, but not to

((Int) (Int)) Int
Kotlin programming language

Functional types #6

In order to obtain instance of a functional types we can use one of the following approaches:

  • Use lambdas expressions: { x: Int, y: Int -> x + y }
  • Use anonymous functions fun(x: Int, y: Int): Int = x + y
  • Use a callable reference to an existing declaration:
    • a top-level, local, member, or extension function: ::isOdd, String::toInt
    • a top-level, member, or extension property: List::size
    • a constructor: ::Regex
Kotlin programming language

Lambda Expressions

Kotlin

Kotlin programming language

Lambda Expressions

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 braces
  • parameter declarations in the full syntactic form go inside curly braces and have optional type annotations.
  • body goes after the -> sign
  • return type is inferred, the last (or possibly single) expression inside the lambda body is treated as the return value
Kotlin programming language

Lambda Expressions #2

val 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

  • using its invoke operator: sum.invoke(1, 2)
  • or just calling: sum(1, 2)
Kotlin programming language

Higher Order Functions (HOF)

Kotlin

Kotlin programming language

Higher Order Functions

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
}
Kotlin programming language

Higher Order Functions #2

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]
Kotlin programming language

Higher Order Functions #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]
  • According to Kotlin convention, if the last parameter of a function is a function
    then a lambda expression passed as the corresponding argument can be placed outside the parentheses
  • This is called a trailing lambda
Kotlin programming language

Higher Order Functions #4

val 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]
  • It's very common for a lambda expression to have only one parameter
  • In that case the parameter will be implicitly declared under the name it
  • it = implicit parameter name
val oddNumbers = listOf(1, 2, 3)
    .filter { it % 2 != 0 } // returns [1, 3]
Kotlin programming language

Higher Order Functions #5

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]
Kotlin programming language

Higher Order Functions #6

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?

Kotlin programming language

Higher Order Functions #7

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

Kotlin programming language

Higher Order Functions #8

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?

Kotlin programming language

Higher Order Functions #9

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

Kotlin programming language

Higher Order Functions #10

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
Kotlin programming language

Higher Order Functions #11

"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
Kotlin programming language

Higher Order Functions #12

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()
Kotlin programming language

Higher Order Functions #13

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, , ]
Kotlin programming language

Higher Order Functions #14

[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]
Kotlin programming language

Higher Order Functions #15

[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]
Kotlin programming language

Higher Order Functions #16

[one, one, was, a, race, horse, two, two, was, one, too, one, one, won, one, race, two, two, won, one, too]
or
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}
Kotlin programming language

Higher Order Functions #17

{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)]
Kotlin programming language

Higher Order Functions #18

[(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)]
Kotlin programming language

Higher Order Functions #19

[(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)]
Kotlin programming language

Higher Order Functions #20

[(a, 1), (horse, 1), (was, 2), (race, 2), (too, 2), (won, 2), (two, 4), (one, 7)]
OR
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)]
Kotlin programming language

Deffered Computations

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)

Kotlin programming language

Deffered Computations #2

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)

Kotlin programming language

Inline Functions

Kotlin

Kotlin programming language

Inline Functions

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
}
Kotlin programming language

Inline Functions #2

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
}
Kotlin programming language

Inlined vs non-inlined in the Byte code

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
})
Kotlin programming language

Scoped Functions

Kotlin

Kotlin programming language

Kotlin standard library contains several functions with sole purpose is to execute a block of code within the context of an object

Kotlin programming language

Scope Functions create a new temporary scope
in which we can declare new local variables
accessible only within this scope

Kotlin programming language

Scoped Functions

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
Kotlin programming language

let function

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) }
Kotlin programming language

let function #2

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)
}
Kotlin programming language

run (extension) function

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” }
}
Kotlin programming language

run (extension) function #2

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()
}
Kotlin programming language

Regular run function

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
}
Kotlin programming language

Regular run function #2

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

Kotlin programming language

Regular run function #3

The return value is the result of the argument lambda function

inline fun <R> run(block: () -> R): R {
    return block()
}
Kotlin programming language

with function

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")
}
Kotlin programming language

with function #2

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()
}
Kotlin programming language

apply function

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"
}
Kotlin programming language

apply function #2

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
}
Kotlin programming language

also function

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 }
Kotlin programming language

also function #2

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
}
Kotlin programming language

Final thought

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/

Kotlin programming language

Thanx!