Enhancing Code Clarity with Reduction in Java and Kotlin
Written on
Chapter 1: Understanding Reduction
Programming with a functional approach enhances the declarative nature and readability of your code. A vital concept in this realm is reduction. When combined with mapping techniques, it offers innovative ways to manipulate and extract data, streamlining your programming experience and easing code maintenance. This article delves into various methods of using reduction and mapReduce in both Java and Kotlin, with the hope of addressing common questions on the topic.
This video provides a clear explanation of reducing in Kotlin collections:
Section 1.1: Simple Reduction in Java
In Java, the simplest form of the reduce function is:
Optional<T> reduce(BinaryOperator<T> accumulator)
This function allows for an operation to be performed on each stream element, collecting results as it goes. Upon invocation, the accumulator.apply(T first, T second) method uses the outcome of the previous operation as the first argument and the current element as the second, with the return value serving as input for the next iteration. The final result from the last iteration becomes the overall return value.
Here’s a practical example:
int addNumbers(final List<Integer> ints) {
return ints.stream()
.reduce((acc, curr) -> acc + curr)
.orElse(0);
}
In this example, the initial value for acc is derived from the first stream element, while curr takes on the next element in the iteration.
The following table illustrates the iterations when using List.of(1, 2, 3, 4, 5):
Thus, the final output of the reduce() function will be Optional(15). The use of Optional is crucial, as it accounts for the possibility of an empty stream, which would otherwise yield Optional.empty().
Section 1.2: Simple Reduction in Kotlin
Kotlin’s equivalent to the above reduce function is:
inline fun <S, T : S> Iterable<out T>.reduce(
operation: (acc: S, T) -> S
): S
A key distinction is that the return type S can be a supertype of type T from the input list. In contrast to Java, an empty list will lead to an UnsupportedOperationException. Here’s how you might implement this in Kotlin:
fun addNumbers(ints: List<Int>): Int {
return ints.reduce { acc, curr -> acc + curr }
}
As in Java, the result from the preceding iteration is utilized for the current iteration.
Chapter 2: Reduction with an Initial Value
Section 2.1: Using Reduction in Java
If you want to begin reduction with a predefined initial value in Java, you can employ:
T reduce(T identity, BinaryOperator<T> accumulator)
This variant accepts T identity as its first argument, serving dual purposes: as the initial value and as a default if the stream is empty. Consequently, this method returns T instead of Optional<T>, alleviating concerns about empty streams.
Here’s a sample function demonstrating this:
String removeSigns(final String input, final List<String> signs) {
return signs.stream()
.reduce(input, (acc, curr) -> acc.replace(curr, ""));
}
When calling this function with:
removeSigns("Str#€ange€ %&s#!ign/&s!",
List.of("!", "#", "€", "%", "&", "/"));
The following iterations are used:
Section 2.2: Using Folding in Kotlin
In Kotlin, this variation of reduce, which includes an initial value, is known as fold():
inline fun <T, R> Iterable<T>.fold(
initial: R,
operation: (acc: R, T) -> R
): R
Here’s how the function would be structured in Kotlin:
fun removeSigns(input: String, signs: List<String>): String {
return signs.fold(input) { acc, curr -> acc.replace(curr, "") }
}
Chapter 3: MapReduce Concept
Kotlin allows for type flexibility in fold(), enabling different types for elements and return types. This flexibility is essential when manipulating elements before reducing them, a process known as MapReduce.
Consider this example involving a Salesperson class:
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)
)
To collect and sum their revenues using Kotlin's fold():
fun sumSalespersonRevenue(salespersons: List<Salesperson>): Double {
return salespersons.fold(0.0) { acc, salesperson ->
acc + salesperson.revenue}
}
In Java, however, types must match. To achieve the same, you need to combine reduce() with map():
double sumSalespersonRevenue(List<Salesperson> salespersons) {
return salespersons.stream()
.map(Salesperson::getRevenue)
.reduce(0.0, Double::sum);
}
This method enhances code readability with method references.
The following video explores how Kotlin enhances Java code:
Chapter 4: Advanced Reduction Examples
Section 4.1: Calculating Pi in Java
For a more complex example, let's calculate π using the Leibniz formula:
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);
}
This function creates a DoubleStream and limits the number of iterations, affecting the precision of the result.
Section 4.2: Calculating Pi in Kotlin
The equivalent function in Kotlin is:
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)
}
Here, we generate a sequence, limit it, and apply similar logic to the Java function.
Final Thoughts
While Java includes a third reduce function, it's less relevant to our discussion, as many reductions can be simplified using explicit combinations of map and reduce operations. Furthermore, there's no Kotlin equivalent for this function.
Another reduction method in Java is through the Stream.collect() method, which you can explore further in Java's tutorial resources.
I wrote this article partly to deepen my understanding of map and reduce concepts. Working daily with Java and Kotlin, it has been beneficial to compare implementations in both languages. I hope you find this guide insightful.
Happy coding with reduction!