Functional Kotlin
Functional programming is a programming style 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. – Bob 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”:
-
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 assign 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 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 an 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.
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 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.
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 arrow
- 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 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 |
let
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
}
with
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")
}
run
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}")
}
apply
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.
also
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 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")
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