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”:
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 theval 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.
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:
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.
valdiscountedPrice=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
fundiscountFiveDollars(price:Double):Double=price-5.0valapplyDiscount=::discountFiveDollars// applyDiscount accepts a Double as an argument and returns a Double
// we use this format when specifying the type
valapplyDiscount:(Double)->Double
For functions with multiple parameters, separate them with a comma.
We can use this notation when explicitly specifying type.
fundiscountFiveDollars(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
valapplyDiscount:(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.
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.
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.
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!
valapplyDiscount={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.
valapplyDiscount:(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:
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.
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).
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:
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.”
valnumbers=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.
valservice=MultiportService("https://example.kotlinlang.org",80)valresult=service.run{port=8080query(prepareRequest()+" to port $port")}
// the same code written with let() function:
valletResult=service.let{it.port=8080it.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.”
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.”
valnumbers=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.
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
vallist=generateSequence(0){it+10}// 0 10 20 30 40 50 60 70 80 ...
valresults=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.
vallist=generateSequence(0){it+10}valresults=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.
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!
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.