35

What is the equivalent of of Scala's great foldLeft in Java 8?

I was tempted to think it was reduce, but reduce has to return something of identical type to what it reduces on.

Example:

import java.util.List;

public class Foo {

    // this method works pretty well
    public int sum(List<Integer> numbers) {
        return numbers.stream()
                      .reduce(0, (acc, n) -> (acc + n));
    }

    // this method makes the file not compile
    public String concatenate(List<Character> chars) {
        return chars.stream()
                    .reduce(new StringBuilder(""), (acc, c) -> acc.append(c)).toString();
    }
}

The problem in the code above is the accumulator: new StringBuilder("")

Thus, could anyone point me to the proper equivalent of the foldLeft/fix my code?

GA1
  • 1,568
  • 2
  • 19
  • 30
  • 2
    FYI: The name of the language is "Scala", not "SCALA". (I believe there is a different language called "SCALA", which is probably not the one you mean.) – Jörg W Mittag Dec 20 '16 at 11:01
  • Related http://stackoverflow.com/questions/30736587/builder-pattern-with-a-java-8-stream – Tunaki Dec 20 '16 at 13:13
  • @JörgWMittag unless you have a source for there being a different language with the same name but capitalized, I would be very surprised. I would think the capitalized spelling comes from old managers who are used to languages being capitalized, like BASIC and FORTRAN :D – nafg Jan 03 '19 at 18:24
  • @nafg: I tried googling for it, but it's kind of hard, since googling for "SCALA" also returns results for "Scala". I believe, I saw it in the context of what we would today called "big data analysis" on IBM midrange systems, but before "big data" (or Scala) were a thing. However, I personally never worked on IBM midrange systems, so I cannot remember the names of the associated tools, frameworks, libraries, or languages, to perform a better google query. The fact that Scala is used in big data, and IBM is heavily pushing Scala doesn't exactly help either. – Jörg W Mittag Jan 03 '19 at 18:33

5 Answers5

34

There is no equivalent of foldLeft in Java 8's Stream API. As others noted, reduce(identity, accumulator, combiner) comes close, but it's not equivalent with foldLeft because it requires the resulting type B to combine with itself and be associative (in other terms, be monoid-like), a property that not every type has.

There is also an enhancement request for this: add Stream.foldLeft() terminal operation

To see why reduce won't work, consider the following code, where you intend to execute a series of arithmetic operations starting with given number:

val arithOps = List(('+', 1), ('*', 4), ('-', 2), ('/', 5))
val fun: (Int, (Char, Int)) => Int = {
  case (x, ('+', y)) => x + y
  case (x, ('-', y)) => x - y
  case (x, ('*', y)) => x * y
  case (x, ('/', y)) => x / y
}
val number = 2
arithOps.foldLeft(number)(fun) // ((2 + 1) * 4 - 2) / 5

If you tried writing reduce(2, fun, combine), what combiner function could you pass that combines two numbers? Adding the two numbers together clearly does not solve it. Also, the value 2 is clearly not an identity element.

Note that no operation that requires a sequential execution can be expressed in terms of reduce. foldLeft is actually more generic than reduce: you can implement reduce with foldLeft but you cannot implement foldLeft with reduce.

dzs
  • 729
  • 6
  • 11
20

Update:

Here is initial attempt to get your code fixed:

public static String concatenate(List<Character> chars) {
        return chars
                .stream()
                .reduce(new StringBuilder(),
                                StringBuilder::append,
                                StringBuilder::append).toString();
    }

It uses the following reduce method:

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

It may sound confusing but if you look at the javadocs there is a nice explanation that may help you quickly grasp the details. The reduction is equivalent to the following code:

U result = identity;
for (T element : this stream)
     result = accumulator.apply(result, element)
return result;

For a more in-depth explanation please check this source.

This usage is not correct though because it violates the contract of reduce which states that the accumulator should be an associative, non-interfering, stateless function for incorporating an additional element into a result. In other words since the identity is mutable the result will be broken in case of parallel execution.

As pointed in the comments below a correct option is using the reduction as follows:

return chars.stream().collect(
     StringBuilder::new, 
     StringBuilder::append, 
     StringBuilder::append).toString();

The supplier StringBuilder::new will be used to create reusable containers which will be later combined.

Lachezar Balev
  • 11,498
  • 9
  • 49
  • 72
  • 13
    Same as with the other answer: *Don’t* use `reduce` this way. The functions are *not* allowed to modify their parameters. The correct usage is `.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)`. See [Mutable reduction](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#MutableReduction). – Holger Dec 20 '16 at 11:03
  • @Holger: thanks, that is true. The answer is updated. – Lachezar Balev Dec 20 '16 at 11:18
  • 5
    This is not about efficiency, it’s about correctness. Using `reduce` this way violates the contract and must be considered broken, even if it may do the intended thing under certain circumstances. Most notably, it will break for sure when using a parallel stream. – Holger Dec 20 '16 at 11:22
  • Do you address the order of the concatenated characters or the statefulness of the builder? – Lachezar Balev Dec 20 '16 at 11:28
  • 3
    It’s about the modification of the `StringBuilder`. There is no problem with the order. – Holger Dec 20 '16 at 11:40
  • It's possible to augment this further by removing the `toString()` at the end if you add a "finisher" to your `Collector`, i.e.: `return chars.stream().collect(Collector.of(StringBuilder::new, StringBuilder::append, StringBuilder::append, StringBuilder::toString)`. I like this approach as it's possible to do something more complicated in the finisher if necessary. – PPartisan Oct 30 '19 at 09:55
  • @Lachezar Balev could you please advice what is wrong here in reduce function https://stackoverflow.com/questions/63843599/reduce-result-datasets-into-single-dataset – BdEngineer Sep 11 '20 at 08:41
  • I was wondering if we make the stream parallel then usage of StringBuilder would be thread safe? – Mr.Q Jun 06 '22 at 06:33
7

The method you are looking for is java.util.Stream.reduce, particularly the overload with three parameters, identity, accumulator, and binary function. That is the correct equivalent to Scala's foldLeft.

However, you are not allowed to use Java's reduce that way, and also not Scala's foldLeft for that matter. Use collect instead.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • 5
    While I like your answer, "you are not allowed" seems a bit wrong. Can you rephrase that? – Sean Patrick Floyd Dec 20 '16 at 11:18
  • 2
    It would be a type error if Java's type system were expressive enough to express that constraint. But it isn't and so, the constraint is only mentioned in the JavaDocs. The JavaDocs say which kinds of objects you are allowed to pass, and the objects the OP passes do not satisfy those constraints, ergo she is not allowed to call `reduce`. How else would you phrase that? – Jörg W Mittag Dec 20 '16 at 11:32
  • 2
    Well, there is no restriction on the type, only on how you use the objects. If you use accumulator and combiner functions like `(a,b) -> new StringBuilder().append(a).append(b)`, it would be a legal usage, though not very efficient, compared to the `collect` solution. – Holger Dec 20 '16 at 11:44
  • 1
    @Holger: I would argue that a "restriction on how you use the objects" *is* a "restriction on the type". Or rather it *should* be, that's the whole point of a type system. It's just that Java's type system is pretty weak and thus these particular constraints cannot be expressed as a type. (Unlike, say, Agda, Guru, Isabelle, Epigram, Idris, Coq, where it would be quite easy to encode this restriction in the type system and thus make it a compile-time type error.) – Jörg W Mittag Dec 20 '16 at 11:54
  • @JörgWMittag I agree it would be nice if Java's type system was expressive enough for this. Alas, it isn't. And there's no point in pretending it is. As I wrote, I am with you, but there's no point in pretending you can forbid something if you actually can't. Any code pattern that compiles and doesn't violate any copyright or license restriction is by definition allowed, whether we like it or not. Which is why I'd rephrase "you are not allowed" to "it's considered an antipattern" or "it's a violation of the method contract". – Sean Patrick Floyd Dec 20 '16 at 13:21
  • 6
    @SeanPatrickFloyd: except it is *not* just an antipattern. It is simply not allowed by the method documentation. Antipatterns may or may not lead to hard to maintain code. The OP's code is simply broken. Kaput. Doesn't work. – Jörg W Mittag Dec 20 '16 at 13:35
  • 9
    This response is plain wrong. Stream's `reduce(identity, accumulator, combiner)` requires an *associative* combiner function, which is not a requirement of `foldLeft`, therefore, not every `foldLeft` construct can be rewritten to `reduce`. Take the following example, where subtraction and division are not associative: ```val ops = List(('+', 1), ('*', 4), ('-', 2), ('/', 5)) val fun: (Int, (Char, Int)) => Int = { case (x, ('+', y)) => x + y case (x, ('-', y)) => x - y case (x, ('*', y)) => x * y case (x, ('/', y)) => x / y } ops.foldLeft(2)(fun) // ((2 + 1) * 4 - 2) / 5``` – dzs May 14 '17 at 08:58
  • FoldLeft works in an ordered way (left-to-right). It doesn't need a combiner. Reduce and collect don't guarantee any execution order. They require a combiner. So reduce and collect are no replacement for foldLeft. – Florian F Jun 16 '21 at 08:17
6

It can be done by using Collectors:

public static <A, B> Collector<A, ?, B> foldLeft(final B init, final BiFunction<? super B, ? super A, ? extends B> f) {
    return Collectors.collectingAndThen(
            Collectors.reducing(Function.<B>identity(), a -> b -> f.apply(b, a), Function::andThen),
            endo -> endo.apply(init)
    );
}

Usage example:

IntStream.rangeClosed(1, 100).boxed().collect(foldLeft(50, (a, b) -> a - b));  // Output = -5000

For your question, this does what you wanted:

public String concatenate(List<Character> chars) {
        return chars.stream()
                .collect(foldLeft(new StringBuilder(), StringBuilder::append)).toString();
}
yi-ji
  • 427
  • 5
  • 8
  • Collectors must respect the "associativity constraint" (see javadoc), which means this code doesn't respect the "Left" part of `foldLeft`. A stream consisting of (1, 2) is allowed to collect by doing `finisher.apply(combiner.apply(accumulator.accept(supplier.get(), 1), accumulator.accept(supplier.get(), 2)))`. If you tried to use `Collectors.reducing` to create cumulative lists (ie, `[[], [1], [1, 2]]`), then you could potentially get `[[], [1], [], [2]]` instead. – Daniel C. Sobral Aug 08 '23 at 20:12
1

Others are correct there's no equivalent though. Here's a util that comes close-

<U, T> U foldLeft(Collection<T> sequence, U identity, BiFunction<U, ? super T, U> accumulator) {
    U result = identity;
    for (T element : sequence)
        result = accumulator.apply(result, element);
    return result;
}

your case using the above method would look like-

public String concatenate(List<Character> chars) {
    return foldLeft(chars, new StringBuilder(""), StringBuilder::append).toString();
}

Or without the lambda method ref sugar,

public String concatenate(List<Character> chars) {
    return foldLeft(chars, new StringBuilder(""), (stringBuilder, character) -> stringBuilder.append(character)).toString();
}
Gagandeep Kalra
  • 1,034
  • 14
  • 13