One of the nicest features of Kotlin is how easy it is to mix and match imperative and functional programming styles. There are many definitions of functional programming, but my personal definition is simply "using functions as input for other functions", maybe this is why it's called 'function'-al programming. The true definition goes a bit further and demands that functions may have no side effects. Given a certain input, they must always produce the same output, these are called pure functions. Another requirement of functional programming is that functions must be treated as first class citizens. If you can assign a value of 1 to a variable, you should also be able to assign a function to a variable.
Lambdas
Since lambdas and the Stream API have been introduced in Java 8, it has become easier to write in a functional style in plain Java as well. Kotlin takes this a bit further as we will see. In Java, you can use Lambdas like this:
// even though Java has the `var` keyword,
// it can not be used here as Lambda expressions in Java require an explicit type
final Function<String, String> f = (String s) -> s.replace(',', '.');
With Kotlin, we can specify a Lambda in a similar way:
val f: (String) -> String = { it.replace(',', '.') }
Here we use some Kotlin magic. First, the type of the Lambda is defined as (String) -> String
which translates to 'a function which takes a String as input and returns a String'. Then, we use the it
keyword inside the Lambda; because the type is known to the compiler, it is inferred that this is a String.
Another way to write it would be:
val f = { s: String -> s.replace(',', '.') }
This looks a bit more like the Java example.
We use Lambdas as input parameters all the time, for instance to perform a map
operation on a list:
List.of("1,000", "2,000").stream().map(item -> item.replace(',', '.'));
// We can also use the lambda we previously assigned to the variable f:
List.of("1,000", "2,000").stream().map(f);
Kotlin has a special recipe for Lambdas, whenever a function takes another function (or lambda) as its last parameter, it can be put outside of the function call inside brackets:
listOf("1,000", "2,000").map({ it.replace(',', '.') })
// Can be rewritten as
listOf("1,000", "2,000").map {
s -> s.replace(',', '.')
}
// And of course we can use the function f here as well:
listOf("1,000", "2,000").map(f)
In Java, it is basically forbidden to use brackets and multi-line lambdas, but because of Kotlin's special notation, this is not that big of an issue. Of course, don't go overboard and keep your lambdas small and concise.
Types of Functions
Depending on the input parameters and return functions, there are various types of interfaces in Java. Function
is the simplest one, because it takes 1 input parameter and returns an actual value, but what if you don't have any input parameter or want to 'return' void? Take a look at this table:
Input Parameters | Return Type | Function name |
---|---|---|
1 | any class | Function |
1 | void | Consumer |
0 | any class | Supplier |
1 | boolean | Predicate |
Java differentiates between primitives and classes, therefore it has specialized primitive alternatives to these, for instance IntFunction
and IntSupplier
.
If you want to use any of these as a parameter, you have to specify the correct type:
public void printer(Object value, Consumer<Object> f) {
f.accept(value);
}
With Kotlin you don't have to remember the names of these functions, since you can define the functions by their signature:
fun printer(value: Any?, f: (Any?) -> Unit) {
f.invoke(value)
}
printer("Hello World!") { println(it) }
Here we define a function printer
which takes a lambda of the form (Any?) -> Unit
where Any?
means an object of any type, can be null, and Unit
is Kotlin's equivalent of Java's void
. We can invoke this function by passing it a lambda as we saw before.
The Scope Family
As we saw in From Java to Kotlin Part 5 there are a lot of useful functions in the 'scope family', such as let
and apply
which let you write in a more functional style. These functions take lambdas as well.
Instead of writing val l = "Hello World!".length
you could write val l = "Hello World!".let { it.length }
. Now it is easy to see that in this example, it would be silly to do so, so use these functions with care and only in places where they improve readability.
Arrow
We can not talk about functional programming in Kotlin without talking about Arrow, a functional programming library available for Kotlin, its purpose:
Arrow aims to bring idiomatic functional programming to Kotlin. This means Arrow is inspired by the great work made in other functional programming communities, yet exposes these ideas and concepts in ways that do not feel alien to Kotlin programmers.
Arrow offers features such as functional error handling (no more try-catch), ways to deal with immutable data and collections and some enhancements for coroutines. If you are interested in functional programming, take a look at Arrow, but you should be careful not to go overboard. Readability is key, if going fully functional reduces readability (and thus maintainability) of your project, you may have lost more than you gained.
Goldilocks Programming
There are many ways to write code which, from the user's perspective, does the exact same thing, so it is up to us, the developers, to write it in a style that our fellow developers can understand (and ourselves one year from now). Functional programming can definitely help us make some parts more readable, but it is still okay to use an 'ancient' for loop in some cases if that makes it more readable. You have to try to find the sweet spot or "Goldilocks zone" where you use enough of a thing that it actually improves your code base, but not so much that it makes it hard to understand for newcomers. If you want to write good code, write it as though you are seeing the code base for the first time. One little piece of advice I can give is that when you are finished with some code, let it simmer one night, come back the following day and you'll be surprised at what you can improve to make it even more clear and readable.