Reducing in Java and Kotlin

Backend Java Kotlin

Uzi Landsmann

Systemutvecklare

Programming in a functional way makes your code declarative and more readable. One of the concepts that I found really important to grasp is reducing. Combine it with mapping and you get new ways to manipulate and extract information from your data, which will make your programming life easier and maintaining your code less challenging. In this article I look at some of the ways you could use reduce and mapReduce in Java and Kotlin, and hope it could help clear up some questions.

 

Simple reduction

Java

Java’s first reduce function is the simplest:

 

Optional<T> reduce(BinaryOperator<T> accumulator)


This function lets you perform an operation on each of the elements of the stream and collect the results. When invoked, the accumulator.apply(T first, T second) function will use the result of the previous operation as the first argument and the current element as the second, and the return value as input for the next iteration. The result of the last iteration is used as the return value of the whole function.

Here’s an example of using this function in java:

 

int addNumbers(final List<Integer> ints) {
    return ints.stream()
            .reduce((acc, curr) -> acc + curr)
            .orElse(0);
}


As mentioned above, acc is the result of the last iteration. However, since such result does not exist the first time around, the first element in the stream is used instead, so in the first iteration, the first element will be used as acc and the second element as curr.

Using List.of(1, 2, 3, 4, 5)as an argument to this function, the following values will be used in each iteration:

 

+-----------+-----+------+--------------+
| iteration | acc | curr | return value |
+-----------+-----+------+--------------+
|         1 |   1 |    2 |            3 |
|         2 |   3 |    3 |            6 |
|         3 |   6 |    4 |           10 |
|         4 |  10 |    5 |           15 |
+-----------+-----+------+--------------+


The return value of the reduce() function will therefore be Optional(15). Why Optional? Because the given Stream might be empty, in which case the function will return Optional.empty() — which is why I had to use the Optional.orElse() function in the example above.


Kotlin


The corresponding reduce function in Kotlin is also simple:

 

inline fun <S, T : S> Iterable<out T>.reduce(
    operation: (acc: S, T) -> S
): S

 

One small difference here is that the return type S can be a supertype of type T, used in the input list. Another is that if the list is empty, the function will throw an UnsupportedOperationException. Here’s an example implementation in Kotlin:

 

fun addNumbers(ints: List<Int>): Int {
    return ints.reduce { acc, curr -> acc + curr }
}


As with Java, the result of a previous iteration will be used as input for the current iteration, and in the first iteration, the first element will be used as acc and the second will be used as the curr (the current value).

 

Reduce with an initial value

Java


If you want to reduce a list, but have an initial value that you want to begin with, you could use Java’s second reduce function:

 

T reduce(T identity, BinaryOperator<T> accumulator)


This function takes T identity as its first argument. It is used for two purposes: as an initial value and as a default value in case the stream is empty. This is also the reason why this function returns T rather than Optional<T> — no need to worry in case the stream is empty.

Here’s an example function that uses this reduce function:

 

String removeSigns(final String input, final List<String> signs) {
    return signs.stream()
            .reduce(input, (acc, curr) -> acc.replace(curr, ""));
}

 

Let’s call this function with some arguments:

 

removeSigns("Str#€ange€ %&s#!ign/&s!", 
        List.of("!", "#", "€", "%", "&", "/"));

 

As before, every iteration is using the result of the previous iteration as input. However in this case, the input for the first iteration is the initial value, named input in the example. The following values are used throughout the invocation:

+-----------+-------------------------+------+---------------------+
| iteration |           acc           | curr |     return value      |
+-----------+-------------------------+------+---------------------+
|         1 | Str#€ange€ %&s#!ign/&s! | !    | Str#€ange€ %&s#ign/&s 
|         2 | Str#€ange€ %&s#ign/&s   | #    | Str€ange€ %&sign/&s   
|         3 | Str€ange€ %&sign/&s     | €    | Strange %&sign/&s     
|         4 | Strange %&sign/&s       | %    | Strange &sign/&s      
|         5 | Strange &sign/&s        | &    | Strange sign/s        
|         6 | Strange sign/s          | /    | Strange signs         
+-----------+-------------------------+------+---------------------+

 

Kotlin

 

This variation of reduce, with an initial value, is called fold() in Kotlin, and looks like this:

inline fun <T, R> Iterable<T>.fold(
    initial: R,
    operation: (acc: R, T) -> R
): R

 

And the example function would look like this in Kotlin:

 

fun removeSigns(input: String, signs: List<String>): String {
    return signs.fold(input) { acc, curr -> acc.replace(curr, "") }
}


MapReduce

Kotlin


One detail you might have noticed, looking at Kotlin’s fold() signature, is that unlike Java, it allows using different types: T is used to denote the type of elements to iterate through, and R is used as the type of the initial value and of the function’s return type. This is important because we sometimes want to manipulate the elements we iterate through before reducing them. This operation is called MapReduce. Let’s use an example: suppose you have a class called Salesperson and have created a list of them:

 

data class Salesperson(
    val name: String,
    val revenue: Double
)

private val salespersons = listOf(
    Salesperson("Nils Nilsson", 1_000_000.0),
    Salesperson("Lars Larsson", 2_000_000.0),
    Salesperson("Bertil Bertilsson", 5_000_000.0)
)

 

Now, suppose you want to use reduce to collect and sum up all of their revenues. Using Kotlin, you could use the fold() function:

 

fun sumSalespersonRevenue(salespersons: List<Salesperson>): Double {
    return salespersons.fold(0.0) { acc, salesperson ->
        acc + salesperson.revenue
    }
}


This is possible because the return type of fold() doesn’t have to be the same as the type of the iterated elements.


Java


In Java, the types do have to be of the same, so you can’t simply use reduce() to achieve the same result. Instead, you have to combine it with a map() operation, which makes sense, as it actually is called MapReduce after all:

 

double sumSalespersonRevenue(List<Salesperson> salespersons) {
    return salespersons.stream()
            .map(Salesperson::getRevenue)
            .reduce(0.0, Double::sum);
}

 

In this case, I also used method references Customer::getRevenue and Double::sum which might make the code even more readable. Of course this is also possible in Kotlin as well. Choose whatever way you find easier to understand and maintain your code.

 

(Slightly more) advanced example

Java

 

Let’s finish up with a slightly more advanced example: calculating π using reduce. According to Leibniz formula for π, you can use the following formula to calculate π:

1-1/3+1/5 -1/7+1/9-… = π/4

So here’s a function that calculates π using this formula and reduce:

 

double calcPi(final long iterations) {
    final var result = DoubleStream
            .iterate(3.0, (x) -> x + 2)
            .limit(iterations)
            .reduce(1.0,
                    (acc, curr) -> {
                        final var res = acc - 1 / curr;
                        return -res;
                    }
            );
    return Math.abs(result * 4);
}

 

We start by creating a DoubleStream using the DoubleStream.iterate() function that takes two arguments: a seed — the initial element and f — a function to be applied to the previous element to produce a new element. This gives us a stream that looks like 3.0, 5.0, 7.0, 9.0 etc. The stream is limitless, so it is up to us to decide how many elements we want to use in our function, which is where the iterations argument is used — to limit the number of iterations, which will in turn affect the precision of the calculation. We then continue by using the reduce function. The initial value we use is the identity 1.0. The doubles in the stream are used as the fraction denominators. For each iteration, we decrease or increase the accumulator by the next (smaller) fraction. We then return a negative result, so that the next calculation will be an increase in case the last one was a decrease and vice versa. Lastly we multiply the result by 4 to get the calculated value of π according to the formula above, and return the absolute value of the calculation. Otherwise, the function would only works with an even number of iterations.

 

Running the function with increasing iterations values will calculate π with increasing precision:

+---------------+--------------------+
|  iterations   |         pi         |
+---------------+--------------------+
| 10            | 3.232315809405594  |
| 100           | 3.1514934010709914 |
| 1_000         | 3.1425916543395442 |
| 1_000_000     | 3.1415936535887745 |
| 1_000_000_000 | 3.1415926545880506 |
+---------------+--------------------+


Kotlin


The corresponding function in Kotlin looks very similar:

 

fun calcPi(iterations: Int): Double {
    val value = generateSequence(3.0, { it + 2 })
        .take(iterations)
        .fold(1.0) { acc, curr ->
            -(acc - 1 / curr)
        }
    return abs(value * 4)
}

 

We’re using the generateSequence() function to create an endless Sequence and then limit it by taking only the first iterations number of elements. We then proceed by using the same logic we used in the Java function, but with Kotlin’s fold() function, discussed above, instead.

 

Last words

There is actually a third reduce function in java, but I do not intend to talk about it here, partly because I don’t think it adds much to the discussion, as it says in the documentation:

Many reductions using this form can be represented more simply by an explicit combination of map and reduce operations.

The second reason is that I can not find an equivalent to this function in Kotlin.

Yet another way to use reduction in Java is by using the Stream.collect() Method. You can read more about how to use both ways in the Java tutorial page about reduction.

 

I wrote this article partially because I wanted a better understanding of the concepts of map and reduce myself. And since I work with Java and Kotlin on daily basis, I thought it would be good to see how to implement the same solutions in both languages. Hopefully you also find this tutorial meaningful.

 

Have fun reducing!

Fler inspirerande inlägg du inte får missa