In Kotlin, functions are the first-class citizens. They are meant to increase reusability and at the same time to reduce the possibility of unsafe programs that are often untestable and buggy. We will take a look at the functional programming features of Kotlin and understand how they help developers to write safe programs.
How to avoid writing unsafe programs
Many bugs come from programs executing incorrectly with the outside world, which are effects that are not easy to test and we have little control of. They are guidelines that can make our programs safer from buggy code.
Avoiding sharing mutable variables
This principle protects programs from accidental sharing of a mutable state of variables between multiple subsequent uses of them, or between uses in multi-thread programs.
Avoiding control structures
Complex control structures often make programs less readable and allow hiding bugs there.
Restricting side effects
A side effect is something that is observable outside of the program and somes with the result returned from the program. Safe programs should be build from functions that take an argument and return a value with no side effects to the outside world.
Not throwing exceptions
Exceptions should be considered the possible result returned from the program instead of wrapping them as unchecked exceptions and throwing them around, which complicates the programs’ logic.
Pure functions
Pure functions have no effects with the outside world and return a value that’s dependent on their arguments. They are deterministic, which means they always return the same result given the same argument. Otherwise, we will never be able to check if the program is correct.
Mathematically a function represents a relationship between a source set and a target set, and all elements in the source set must have one and only one element in the target set. For example, a function f(x) = 2 * x, for positive integers x > 0, it represents the relationship between a source set (1, 2, 3, 4, 5, 6, 7, 8, 9, 10,…) and target set (1, 2, 3, 4, 5, 6, 7, 8, 9, 10,…). But the inverse is not a function. The set of elements in the target set that have a corresponding element in the source set is called the image of the function.
We can view a function that only maps from a source set to a target set and does nothing. It doesn’t have the capability of multiplying integers by 2. We can say the expression f(x) is equivalent to the expression (2 * x), and they are interchangeable.
If a relationship represented by a function can not be defined for all elements of the source set, the function is called a partial function. For example, f(x) = 1/x, are not defined for all elements in the source set of integers including zero. For a partial function to be a total function, we must make the target set as the set of rational numbers including an error, which indicates 1/0. This is very important in pure functional programming, in which many bugs come from applying partial functions, instead of total functions. In short, total functions are functions, partial functions are not functions. Programming total functions is always safer.
Functions can be reused and composed to build other functions. If we have two functions: f(x) and g(x). We can compose them in one way in which x in f(x) is a placeholder for g(x), so the resulting function will be f(g(x)), and also can be denoted as f。g(x). Or the other way in which x in g(x) is a placeholder for f(x), the resulting function will be g(f(x)), denoted as g。f(x). We can find manny examples such that f。g(x) does not equal to g。f(x).
Considering a function f(x, y) = x + y for positive integers x, y in mathematical definition (a relationship between one source set and one target set), we can say the source set consists of the possible pairs of two integers set, which is still one set. So the function is a relationship between a tuple of two elements in the source set and the elements in the target set. We can denote the function as f((x, y)) = x + y. If we can view this function as f(x)(y) = x + y and apply this function to the argument x, the source set of x then maps to the target set of functions g with each x as a constant. That is g = f(x) and yields g(y) = x + y where x is a constant. In short, the result of f(x) is a function g, and the result of apply g to an integer y is is an integer, f(x)(y) is the currying of f(x, y). It is like memorizing partial computations or algorithms and applying to further computations.
From the above discussion, we can say that pure functions only return values (including returning exceptions but not throwing them) and does nothing else. Pure functions don’t mutate outside world (no effects) and don’t change arguments. Pure functions should always return the same value from the same argument. Considering the following example in Kotlin and see if the functions defined there meets the requirements of pure functions.
class ExampleFunctions {
var rate1 = 3
val rate2 = 4
// 1. Both a, b are immutable and the return value only depends // on arguments. No side effect. It is a pure function
fun add(a: Int, b: Int): Int = a + b
// 2. It is a constant function and not dependent on arguments. a, b // are immutable. No side effect. It is a pure function
fun multiply(a: Int, b: Int?): Int = 8
// 3. It will throw an exception if the divisor b is 0. It isn't a // pure function.
fun divide(a: Int, b: Int): Int = a / b
// 4. It depends on public mutable variable rate1. It cannot
// guarantee it will always return the same result with the same
// argument a between two function calls. It is not a pre function.
fun price1(a: Int): Int = a / 100 * (100 + rate1)
// 5. It depends on public immutable variable rate2 and can
// guarantee it will always return the same result with the same
// argument a between two function calls. It is a pure function.
fun price2(a: Int): Int = a / 100 * (100 + rate2)
// 6. It will not throw an exception even if the divisor b is 0.0,
// in that the function will return a value Infinity of the type
// Double. It is a pure function.
fun divide(a: Double, b: Double): Double = a / b
}
In the above function 4, we can consider the function has the enclosing class a as its implicit parameter. Therefore, we can redefine the function to make it a pure function. Furthermore, functions that don’t access the enclosing class instance can be moved out of the class, into either the companion object or the package level in Kotlin.
class ExampleFunctions {
var rate1 = 3
val rate2 = 4
... fun price1(a: Int): Int = a / 100 * (100 + rate1) companion object {
fun price2(c: ExampleFunctions, a: Int): Int =
a / 100 * (100 + c.rate2)
}
}
Functions as values and values as functions
In the functional programming paradigm, functions can be used as values and values can represent functions. In Kotlin and many other languages, a function has its type just as data has its type. Functions can be treated as data, such as being assigned to references of its type, being passed as arguments to other functions, being stored in data structures , and being returned from other functions, just like other data types are used. In Kotlin, we can use lambda expressions to define functions under the functional programming paradigm. The lambda expression consists of parentheses, parameter names, an arrow and expression to be evaluated and returned.
val add: (Int, Int) -> Int = { x, y -> x + y }
We have to note that add
is not the name of the function, it is a reference of the type (Int, Int) -> Int
to which the lambda is assigned. In the functional programming paradigm and in Kotlin, a function has no name, a lambda has no name, a Kotlin fun
is not a function but like a method in java. However, a Kotlin fun
can have attributes of a pure function as long as it meets all the requirements of a pure function we have discussed. A Kotlin fun
cannot be treated as data. Lambda expressions in Kotlin are used as the mechanisms for the functional programming such that functions are values and values are functions.
Function references in Kotlin
Similar to method references in Java, in Kotlin function references can use a regular fun
function in a lambda.
class ExampleFunctions {
fun add(a: Int, b: Int): Int = a + b
companion object {
fun rate1(r: Double): Double = (100 + r) / 100
}
}
fun double(n: Int): Int = n * 2val funcs = ExampleFunctions()
val addIntegers: (Int, Int) -> Int = funcs::add
val doubleInteger: (Int) -> Int = ::double
val applyRate: (Double) -> Double = (ExampleFunctions)::rate1
Function Composition
Composing functions is not the same as calling multiple functions sequentially and use previous function’s output as following functions input. It should be an operation between functions that produce one resulting, composed function. It is just like an operator between numbers in programming languages.
fun double(n: Int): Int = n * 2
fun successor(n: Int): Int = n + 1
// This is not function composition
println(double(successor(18)))
We can define a Kotlin fun
function that composes two functions with the same type and produces a function with the same type as its component functions using lambda.
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int = {
x -> f(g(x)) }
Or it can be simplified via Kotlin type inference feature.
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int = {
f(g(it)) }
And the usage would be:
val successorThenDouble = compose(::double, ::successor)
println(successorThenDouble(3))
Function composition can also be generalized using Kotlin type parameters. However, we have to be careful about the types of parameters and the order of implementation to be able to compile.
fun <T, U, V> compose(f: (U) -> V, g: (T) -> U): (T) -> V = {
f(g(it)) }
Currying Functions
We know from the previous mathematical discussion about multiple arguments to a function is really a tuple to a function, and the currying of function f(x, y) is f(x)(y). This can be described as applying arguments to the function one by one and returning the immediate functions to apply the subsequent arguments to them and finally returning values/data. In the previous add
function for two integers, we can redefine it using lambda so that we apply arguments one by one, return intermediate function of type (Int) -> Int
, and finally return Int
.
val add: (Int) -> (Int) -> Int = { a -> { b -> a + b } }
println(add(3)(8))
Higher-order Functions
A higher-order function is a function as value, taking functions as arguments and returning functions. We can redefine the previous fun compose()
as a function as a value and apply currying to handle two arguments, which are also functions of type (Int) -> Int
. So the type of the curried compose function as value is ((Int) -> Int) -> ((Int) -> Int) -> (Int) -> Int
.
val compose: ((Int) -> Int) -> ((Int) -> Int) -> (Int) -> Int =
{ x -> { y -> { z -> x(y(z)) } } }
And the equivalent form would be the following.
val compose = { x: (Int) -> Int -> { y: (Int) -> Int -> { z: Int ->
x(y(z)) } } }
The usage would be the following.
val double: (Int) -> Int = { it * 2 }
val successor: (Int) -> Int = { it + 1 }val successorThenDouble = compose(double)(successor)
Conclusions
In this article, we first focus on functional programming mathematically and understand the fundamental mechanisms and the syntax that Kotlin provides to promote safer programming and reusability. There are more advanced pure functional programming concepts and applications in Kotlin that we can learn later on.
Hope you can enjoy the functional programming in Kotlin.