CS 346 (W23)
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Functional Kotlin

Introduction

Functional programming is a programming style1 where programs are constructed by compositing functions together. Functional programming treats functions as first-class citizens: they can be assigned to a variable, passed as parameters, or returned from a function.

Functional programming also specifically avoids mutation: functions transform inputs to produce outputs, with no internal state. Functional programming can be described as declarative (describe what you want) instead of imperative (describe how to accomplish a result).

Functional programming constrains assignment, and therefore constrains side-effects (Martin 2003).

Kotlin is considered a hybrid language: it provides mechanisms for you to write in a functional style, but it also doesn’t prevent you from doing non-functional things. As a developer, it’s up to you to determine the most appropriate approach to a given problem.

Here are some common properties that we talk about when referring to “functional programming”:

Functional Programming Paradigm - https://towardsdatascience.com

First-class functions means that functions are treated as first-class citizens. We can pass them as to another function as a parameter, return functions from other functions, and even assignment functions to variables. This allows us to treat functions much as we would treat any other variable.

Pure functions are functions that have no side effects. More formally, the return values of a pure function are identical for identical arguments (i.e. they don’t depend on any external state). Also, by having no side effects, they do not cause any changes to the system, outside of their return value. Functional programming attempts to reduce program state, unlike other programming paradigms (imperative or object-oriented which are based on careful control of program state).

Immutable data means that we do not modify data in-place. We prefer immutable data that cannot be accidentally changed, especially as a side-effect of a function. Instead, if we need to mutate data, we pass it to a function that will return a new data structure containing the modified data, leaving the original data intact. This avoids unintended state changes.

Lazy evaluation is the notion that we only evaluate as expression when we need to operate on it (and we only evaluate what we need to evaluate at the moment!) This allows us to express and manipulate some expressions that would be extremely difficult to actually represent in other paradigms.

In the next sections, we’ll focus on Kotlin support for higher-order functions. Avoiding mutation and side effects is partly a stylistic choice - you don’t require very many language features to program this way, but Kotlin encourages non-mutable data with the val keyword.

Function Types

Functions in Kotlin are “first-class citizens” of the language. This means that we can define functions, assign them to variables, pass functions as arguments to other functions, or return functions! Functions are types in Kotlin, and we can use them anywhere we would expect to use a regular type.

Dave Leeds on Kotlin presents the following excellent example:

Bert’s Barber shop is creating a program to calculate the cost of a haircut, and they end up with 2 almost-identical functions.

fun main() { val taxMultiplier = 1.10 fun calculateTotalWithFiveDollarDiscount(initialPrice: Double): Double { val priceAfterDiscount = initialPrice - 5.0 val total = priceAfterDiscount * taxMultiplier return total } fun calculateTotalWithTenPercentDiscount(initialPrice: Double): Double { val priceAfterDiscount = initialPrice * 0.9 val total = priceAfterDiscount * taxMultiplier return total } }

These functions are identical except for the line that calculates priceAfterDiscount. If we could somehow pass in that line of code as an argument, then we could replace both with a single function that looks like this, where applyDiscount() represents the code that we would dynamically replace:

// applyDiscount = initialPrice * 0.9, or
// applyDiscount = initialPrice - 5.0
fun calculateTotal(initialPrice: Double, applyDiscount: ???): Double {
    val priceAfterDiscount = applyDiscount(initialPrice)
    val total = priceAfterDiscount * taxMultiplier
    return total
}

This is a perfect scenario for passing in a function!

Assign a function to a variable.

fun discountFiveDollars(price: Double): Double = price - 5.0
val applyDiscount = ::discountFiveDollars

In this example, applyDiscount is now a reference to the discountFiveDollars function (note the :: notation when we have a function on the RHS of an assignment). We can even call it.

val discountedPrice = applyDiscount(20.0) // Result is 15.0

So what is the type of our function? The type of a function is the function signature, but with a different syntax that you might be accustomed to seeing.

// this is the original function signature
fun discountFiveDollars(price: Double): Double = price - 5.0
val applyDiscount = ::discountFiveDollars

// applyDiscount accepts a Double as an argument and returns a Double
// we use this format when specifying the type
val applyDiscount: (Double) -> Double

For functions with multiple parameters, separate them with a comma.

We can use this notation when explicitly specifying type.

fun discountFiveDollars(price: Double): Double = price - 5.0

// specifying type is not necessary since type inference works too
// we'll just do it here to demonstrate how it would appear
val applyDiscount : (Double) -> Double = ::discountFiveDollars

Pass a function to a function

We can use this information to modify the earlier example, and have Bert’s calculation function passed into the second function.

fun discountFiveDollars(price: Double): Double = price - 5.0
fun discountTenPercent(price: Double): Double = price * 0.9
fun noDiscount(price: Double): Double = price

fun calculateTotal(initialPrice: Double, applyDiscount: (Double) -> Double): Double {
    val priceAfterDiscount = applyDiscount(initialPrice)
    val total = priceAfterDiscount * taxMultiplier
    return total
}

val withFiveDollarsOff = calculateTotal(20.0, ::discountFiveDollars) // $16.35
val withTenPercentOff  = calculateTotal(20.0, ::discountTenPercent)  // $19.62
val fullPrice          = calculateTotal(20.0, ::noDiscount)          // $21.80

Returning Functions from Functions

Instead of typing in the name of the function each time he calls calculateTotal(), Bert would like to just enter the coupon code from the bottom of the coupon that he receives from the customer. To do this, he just needs a function that accepts the coupon code and returns the right discount function.

fun discountForCouponCode(couponCode: String): (Double) -> Double = when (couponCode) {
    "FIVE_BUCKS" -> ::discountFiveDollars
    "TAKE_10"    -> ::discountTenPercent
    else         -> ::noDiscount
}
I’ve taken liberties with Dave Leed’s example, but my notes can’t do it justice. I’d highly recommend a read through his site - he’s building an outstanding Kotlin book chapter-by-chapter with cartoons and illustrations.

Introduction to Lambdas

We can use this same notation to express the idea of a function literal, or a function as a value.

val applyDiscount: (Double) -> Double = { price: Double -> price - 5.0 }

The code on the RHS of this expression is a function literal, which captures the body of this function. We also call this a lambda. A lambda is just a function, but written in this form:

  • the function is enclosed in curly braces { }
  • the parameters are listed, followed by an arror
  • the body comes after the arrow

What makes a lambda different from a traditional function is that it doesn’t have a name. In the expression above, we assigned the lambda to a variable, which we could them use to reference it, but the function itself isn’t named.

Note that due to type inference, we could rewrite this example without the type specified on the LHS. This is the same thing!

val applyDiscount = { price: Double -> price - 5.0 }

The implicit ‘it’ parameter

In cases where there’s only a single parameter for a lambda, you can omit the parameter name and the arrow. When you do this, Kotlin will automatically make the name of the parameter it.

val applyDiscount: (Double) -> Double = { it - 5.0 }

Lambdas and Higher-Order Functions

Passing Lambdas as Arguments

Higher-order functions have a function as an input or output. We can rewrite our earlier earlier example to use lambdas instead of function references:

// fun discountFiveDollars(price: Double): Double = price - 5.0
// fun discountTenPercent(price: Double): Double = price * 0.9
// fun noDiscount(price: Double): Double = price

fun calculateTotal(initialPrice: Double, applyDiscount: (Double) -> Double): Double {
    val priceAfterDiscount = applyDiscount(initialPrice)
    val total = priceAfterDiscount * taxMultiplier
    return total
}

val withFiveDollarsOff = calculateTotal(20.0, { price - 5.0 }) // $16.35
val withTenPercentOff  = calculateTotal(20.0, { price * 0.9 }) // $19.62
val fullPrice          = calculateTotal(20.0, { price })       // $21.80

In cases where function’s last parameter is a function type, you can move the lambda argument outside of the parentheses to the right, like this:

val withFiveDollarsOff = calculateTotal(20.0) { price -> price - 5.0 }
val withTenPercentOff  = calculateTotal(20.0) { price -> price * 0.9 }
val fullPrice          = calculateTotal(20.0) { price -> price }

This is meant to be read as two arguments: one inside the brackets, and the lambda as the second parameter.

Returning Lambdas as Function Results

We can easily modify our earlier function to return a lambda as well.

fun discountForCouponCode(couponCode: String): (Double) -> Double = when (couponCode) {
    "FIVE_BUCKS" -> { price -> price - 5.0 }
    "TAKE_10"    -> { price -> price * 0.9 }
    else         -> { price -> price }
}

Scope Functions

The Kotlin standard library contains several functions whose sole purpose is to execute a block of code on an object. When you call such a function on an object with a lambda expression, it forms a temporary scope, and applies the lambda to that object.

There are five of these scope functions: let, run, with, apply, and also, and each of them has a slightly different purpose.

Here’s an example where we do not use one of these scope functions. There is a great deal of repetition, since we need a temporary variable, and then have to act on that object.

val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

With a scope function, we can refer to the object without using a name. This is greatly simplified!

Person("Alice", 20, "Amsterdam").let {
    println(it)
    it.moveTo("London")
    it.incrementAge()
    println(it)
}

The scope functions have subtle differences in how they work, summarized from the Kotlin Standard Library documentation. Inside the lambda of a scope function, the context object is available by a short reference instead of its actual name. Each scope function uses one of two ways to access the context object: as a lambda receiver (this) or as a lambda argument (it).

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

The context object is available as an argument (it). The return value is the lambda result.

let can be used to invoke one or more functions on results of call chains. For example, the following code prints the results of two operations on a collection:

val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)    

With let, you can rewrite it:

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let { 
    println(it)
    // and more function calls if needed
} 

A non-extension function: the context object is passed as an argument, but inside the lambda, it’s available as a receiver (this). The return value is the lambda result. We recommend with for calling functions on the context object without providing the lambda result. In the code, with can be read as “with this object, do the following.”

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

The context object is available as a receiver (this). The return value is the lambda result. run does the same as with but invokes as let - as an extension function of the context object. run is useful when your lambda contains both the object initialization and the computation of the return value.

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}
// the same code written with let() function:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

The context object is available as a receiver (this). The return value is the object itself. Use apply for code blocks that don’t return a value and mainly operate on the members of the receiver object. The common case for apply is the object configuration. Such calls can be read as “apply the following assignments to the object.”

val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
println(adam)

Having the receiver as the return value, you can easily include apply into call chains for more complex processing.

The context object is available as an argument (it). The return value is the object itself. also is good for performing some actions that take the context object as an argument. Use also for actions that need a reference to the object rather than its properties and functions, or when you don’t want to shadow the this reference from an outer scope. When you see also in the code, you can read it as “and also do the following with the object.”

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

Collection Functions

Collection classes (e.g. List, Set, Map, Array) have built-in functions for working with the data that they contain. These functions frequently accept other functions as parameters.

filter produces a new list of those elements that return true from a predicate function.

val list = (1..100).toList()
val filtered = list.filter { it % 5 == 0 }
// 5 10 15 20 ... 100

val below50 = filtered.filter { it in 0..49 }
// [5, 10, 15, 20]

map produces a new list that is the results of applying a function to every element that it contains.

val list = (1..100).toList()
val doubled = list.map { it * 2 }
// 2 4 6 8 ... 200

reduce accumulates value starting with the first element and applying an operation to each element from left to right.

val strings = listOf("a", "b", "c", "d")
println(strings.reduce { acc, string -> acc + string }) // abcd

zip combines two collections together, associating their respective pairwise elements.

val foods = listOf("apple", "kiwi", "broccoli", "carrots")
val fruit = listOf(true, true, false, false)

// List<Pair<String, Boolean>>
val results = foods.zip(fruit)
// [(apple, true), (kiwi, true), (broccoli, false), (carrots, false)]

A more realistic scenario might be where you want to generate a pair based on the results of the list elements:

val list = listOf("123", "", "456", "def")
val exists = list.zip(list.map { !it.isBlank() })
// [(123, true), (, false), (456, true), (def, true)]

val numeric = list.zip(list.map { !it.isEmpty() && it[0] in ('0'..'9') })
[(123, true), (, false), (456, true), (def, false)]

forEach calls a function for every element in the collection.

val fruits = listOf("advocado", "banana", "cantaloupe" )
fruits.forEach { print("$it ") }
// advocado banana cantaloupe 

We also have helper functions to extract specific elements from a list.

take returns a collection containing just the first n elements. drop returns a new collection with the first n elements removed.

val list = (1..50)
val first10 = list.take(10) 
// 1 2 3 ... 10
val last40 = list.drop(10) 
// 11 12 13 ... 50

first and last return those respective elements. slice allows us to extract a range of elements into a new collection.

val list = (1..50)
val even = list.filter { it % 2 == 0 }
// 2 4 6 8 10 ... 50

even.first()	// 2
even.last() // 50
even.slice(1..3) // 4 6 8

Lazy Sequences

Lazy evaluation allows us to generate expressions representing large or infinite lists, and work on them without actually evaluating every element . For example, we can generate an infinite sequence and then extract the first n elements that we need.

// generate an infinite list of integers
// starting at zero, step 10
val list = generateSequence(0) { it + 10}
// 0 10 20 30 40 50 60 70 80 ... 
val results = list.take(5).toList()
// 0 10 20 30 40 

take from this list before attempting to do anything with it. It’s infinite so it’s possible to hang your system if you’re not careful2.

val list = generateSequence(0) { it + 10}
val results = list.drop(5).toList() // length is infinite - 5 ?!? 

Chaining operations

Since our higher-order functions typically return a list, we can chain operations together, so the return value of one function is a list, which is acted on by the next function in the chain. For example, we can map and filter a collection without needing to store the intermediate collection.

val list = (1..999999).toList()
val results = list
	.map { it * 2 }
	.take(10)
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

The operations are performed in top-down order: map, then take. In this case, it means that we’re mapping the entire list and then discarding most of the resulting list with the take operation. This is really inefficient: filter your list first!

// better implementation
val veryLongList = listOf(0..9999999L).toList()
val results = veryLongList
	.take(50)
	.map { it * 2 }
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

Recursion

As a hybrid language, Kotlin supports a number of paradigms. Recursion is less likely than other languages, given that we have loops and other mechanisms to handle iteration.

However, the compiler certainly supports recursion, and can even optimize for tail recursion. To qualify, a function needs to:

  • be structured so that the last statement is a call to the function, with state being passed in the function call.
  • use the tailrec keyword.
import java.math.BigInteger

tailrec fun fibonacci(n: Int, a: BigInteger, b: BigInteger): BigInteger {
    return if (n == 0) a else fibonacci(n-1, b, a+b)
}

fun main(args: Array<String>) {
    println(fibonacci(100, BigInteger("0"), BigInteger("1")))
}
// 354224848179261915075

https://www.programiz.com/kotlin-programming/recursion

Functional Programming

https://xkcd.com/1270/


  1. Other popular styles being imperative and object-oriented programming. ↩︎

  2. Yes I tried this! The result was a runtime memory error: “Exception in thread “main” java.lang.OutOfMemoryError: Java heap space”. ↩︎