46

Simple reduce on an empty array will throw:

Exception in thread "main" java.lang.UnsupportedOperationException: Empty iterable can't be reduced.

The same exception when chaining:

val a = intArrayOf()

val b = a.reduce({ memo, next -> memo + next }) // -> throws an exception

val a1 = intArrayOf(1, 2, 3)

val b1 = a.filter({ a -> a < 0 }).reduce({ a, b -> a + b }) // -> throws an exception

Is it the expected operation of the reduce or is it a bug?

Are there any workarounds?

dippe
  • 561
  • 1
  • 4
  • 4
  • 4
    FYI: If you are just using `reduce` for summation, then you can use the builtin `.sum()` on lists of numbers and if you are summing stuff that is a bit special, then you can write an extension function to help out as well. – Richard Green Nov 23 '16 at 17:47

4 Answers4

83

The exception is correct, reduce does not work on an empty iterable or array. What you're probably looking for is fold, which takes a starting value and an operation which is applied successively for each element of the iterable. reduce takes the first element as a starting value, so it needs no additional value to be passed as an argument, but requires the collection to be not empty.

Example usage of fold:

println(intArrayOf().fold(0) { a, b -> a + b })  // prints "0"
Alexander Udalov
  • 31,429
  • 6
  • 80
  • 66
  • Thank you, the fold seems the solution :) So as I can understand the chained filtering and reducing is risky because the filter can give back an empty list and the reduce will always be called so exception will thrown. – dippe Feb 28 '16 at 06:29
  • 1
    Why don't you define `reduce` similarly to how it's defined in ECMAScript 6? Let `reduce` take two parameters, the function and an initial value. If no initial value is defined (aka `null`), `reduce` assumes the first value in the array as initial value. – Stacky Nov 14 '17 at 00:09
  • 1
    Did they call it `reduce`? If so, people looking for `fold` may not realize they need to `reduce` when they want to `fold`. If they also provide `fold`, then they've created two ways to do the exact same thing. Either way, that seems like it would create additional barriers for programmers. I don't think it would be appropriate for the language; however, we can still achieve the same thing by creating our own function if we find we need/want that sort of shorthand. – methodsignature Sep 11 '18 at 17:38
18

I just want to add more general approach for situations where it is not possible to use fold(...). Because, in order to use fold you need to be able to express some initial value.

someIterable
    .filter { TODO("possibly filter-out everything") }
    .takeIf { it.isNotEmpty() }
    ?.reduce { acc, element -> TODO("merge operation") }
    ?: TODO("value or exception for empty")

With this approach, in case of an empty collection, reduce won't be executed because takeIf will convert it to null. And at the end we can use elvis operator to express some value (or throw exception) in that case.

Your example:

intArrayOf(1, 2, 3)
    .filter { a -> a < 0 }
    .takeIf { it.isNotEmpty() }
    ?.reduce { a, b -> a + b }
    ?: 0

EDIT:

Since kotlin 1.4, there is stdlib function reduceOrNull which is more suitable than .takeIf { it.isNotEmpty() } approach.

Generic case:

someIterable
    .filter { TODO("possibly filter-out everything") }
    .reduceOrNull { acc, element -> TODO("merge operation") }
    ?: TODO("value or exception for empty")

Used for your problem:

intArrayOf(1, 2, 3)
    .filter { a -> a < 0 }
    .reduceOrNull { a, b -> a + b }
    ?: 0
Antonio Tomac
  • 438
  • 5
  • 12
3
public inline fun <S, T : S> List<T>.reduceRightDefault(defaultIfEmpty: S, operation: (T, acc: S) -> S): S {
    return if (isEmpty()) defaultIfEmpty
    else reduceRight(operation)
}

usage:

val result = listOf<Boolean>().reduceRightDefault(false) { first, second -> first && second}

println("result $result")//result false
NickUnuchek
  • 11,794
  • 12
  • 98
  • 138
-1

you can use foldRight:

println(listOf("1", "2", "3")
    .filter { "not found" == it }
    .foldRight("") { a, b -> a + b }) // prints: ""

println(listOf("1", "2", "3")
    .filter { "not found" != it }
    .foldRight("") { a, b -> a + b }) // prints: "123"

or in your case:

val a = intArrayOf()
val b = a.foldRight(0) { memo, next -> memo + next } // b == 0
val a1 = intArrayOf(1, 2, 3)
val b1 = a.filter { a -> a < 0 }.foldRight(0) { a, b -> a + b } // b1 == 0
Maksim Kostromin
  • 3,273
  • 1
  • 32
  • 30