2

I want to use Stream API to keep track of a variable while changing it with functions.

My code:

public String encoder(String texteClair) {
    for (Crypteur crypteur : algo) {
        texteClair = crypteur.encoder(texteClair);
    }
    return texteClair;
}

I have a list of classes that have methods and I want to put a variable inside all of them, like done in the code above.

It works perfectly, but I was wondering how it could be done with streams?

Could we use reduce()?

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • Is `encoder()` associative? – shmosel May 03 '22 at 04:39
  • No it is not, it returns a String, to give a bit of context, It's a list of classes of ways to crypt a string, and I want to use multiple on top of each other – Cédric Bélanger-St-Pierre May 03 '22 at 04:44
  • If it's not associative, it's not parallelizable, and it won't work with streams. – shmosel May 03 '22 at 04:48
  • 2
    @shmosel not all streams need to be parallel. This is one clear case of a stream that needs to be sequential and ordered; although you're making a good case in principle. – ernest_k May 03 '22 at 05:18
  • @ernest_k The streams API is designed to support parallelization and doesn't make exceptions for sequential usage. – shmosel May 03 '22 at 05:19
  • 2
    If I write a sequential stream pipeline, and realize that the implementation arbitrarily parallelized it during execution, I'd report a bug. @shmosel – ernest_k May 03 '22 at 05:21
  • @ernest_k I'm not suggesting it could be arbitrarily parallelized. I'm saying the the API imposes requirements like statelessness and associativity without any special allowances for sequential streams. You could probably violate the spec and get away with it, but I wouldn't recommend it. – shmosel May 03 '22 at 05:29
  • Again, you're right in principle; and I don't want to argue that. It's just wrong to imply that sequential streams work by happy accident. The API encourages properties that would make parallel execution safe (for the developer's ability to easily parallelize the stream, imo); but it does not make efforts to discourage an execution with primarily sequential-demanding properties. Now, this is becoming subtle. If they meant to discourage people from writing stateful operations, they'd have removed the possibility and left much of that to the discretion of the stream implementation. @shmosel – ernest_k May 03 '22 at 06:02
  • @ernest_k The Javadoc for `reduce()` says it expects "an associative, non-interfering, stateless function for combining two values." Is that what you mean by "encouraging"? What would "removing the possibility" look like? – shmosel May 03 '22 at 06:42
  • See also: https://stackoverflow.com/questions/34247318/does-stream-foreach-respect-the-encounter-order-of-sequential-streams – shmosel May 03 '22 at 06:47
  • 1
    @shmosel Nope, I wouldn't say it "encourages", that's pretty close to *demanding*. But that's `reduce`. Previous comments generalized that to streams. There are many safe ways of solving for this problem with streams (arguably, `reduce()` can be one of them in practical terms, although you'd be right to veto that in code reviews - I would too) – ernest_k May 03 '22 at 06:53
  • 1
    @ernest_k it doesn’t matter which terminal operation you’ll use; if your solution requires the Stream to run sequentially to function correctly, there’s at least one formal rule you’re violating. Bohemian’s answer could be fixed by using `forEachOrdered`, then it would be formally correct but that doesn’t imply that I would not veto such a solution in code reviews. If you think there is a Stream solution that justifies replacing the straight-forward loop, I’d be happy to see it. – Holger May 04 '22 at 10:16
  • @Holger Right. Again, shmosel's comments are right in principle; so is your observation. You probably got to the core of my whinging: it isn't so much to insist that a working solution can be devised using a stream + operations with inadequate properties; it's rather that you can't use a stream with sequential operations and get formal guarantees of consistency. There's more to just having to revert to a for-loop (which I agree is *the* conforming way): the style is different... and buggy imperative code can benefit from built-in implementation. iow, it's a pity they had to (over)simplify it. – ernest_k May 04 '22 at 10:58

2 Answers2

1

Use an AtomicReference, which is effectively final, but its wrapped value may change:

public String encoder(String texteClair) {
    AtomicReference<String> ref = new AtomicReference<>(texteClair);
    algo.stream().forEach(c -> ref.updateAndGet(c::encoder)); // credit Ole V.V
    return ref.get();
}
Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • This doesn't use streams. – shmosel May 03 '22 at 06:48
  • Or for lovers of method references: `algo.forEach(c -> ref.updateAndGet(c::encoder));`. @shmosel correct, it does even better by using `Collection.forEach()`. – Ole V.V. May 03 '22 at 06:56
  • @shmosel it does now :) – Bohemian May 03 '22 at 07:58
  • 2
    A good example why bringing in Streams at all costs is not a good idea. `algo.forEach(…)` will iterate in the collection’s order if there’s a defined order, whereas `algo.stream().forEach(…)` is an explicitly *unordered* operation. The correct call would be `algo.stream().forEachOrdered(…)` so if the goal was to make the code more complicated for no benefit, it’s working even better. – Holger May 04 '22 at 09:41
1

Could we use reduce()?

I guess we could. But keep in mind that it's not the best case to use streams.

Because you've mentioned "classes" in plural, I assume that Crypteur is either an abstract class or an interface. As a general rule you should favor interfaces over abstract classes, so I'll assume the that Crypteur is an interface (if it's not, that's not a big issue) and it has at least one implementation similar to this :

public interface Encoder {
    String encoder(String str);
}

public class Crypteur implements Encoder {
    private UnaryOperator<String> operator;
    
    public Crypteur(UnaryOperator<String> operator) {
        this.operator = operator;
    }
    
    @Override
    public String encoder(String str) {
        return operator.apply(str);
    }
}

Then you can utilize your encoders with stream like this:

public static void main(String[] args) {
    List<Crypteur> algo =
        List.of(new Crypteur(str -> str.replaceAll("\\p{Punct}|\\p{Space}", "")),
                new Crypteur(str -> str.toUpperCase(Locale.ROOT)),
                new Crypteur(str -> str.replace('A', 'W')));
    
    String result = encode(algo, "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system");

    System.out.println(result);
}

public static String encode(Collection<Crypteur> algo, String str) {
    return algo.stream()
        .reduce(str,
            (String result, Crypteur encoder) -> encoder.encoder(result),
            (result1, result2) -> { throw new UnsupportedOperationException(); });
}

Note that combiner, which is used in parallel to combine partial results, deliberately throws an exception to indicate that this task ins't parallelizable. All transformations must be applied sequentially, we can't, for instance, apply some encoders on the given string and then apply the rest of them separately on the given string and merge the two results - it's not possible.

Output

EVERYPIECEOFKNOWLEDGEMUSTHWVEWSINGLEUNWMBIGUOUSWUTHORITWTIVEREPRESENTWTIONWITHINWSYSTEM
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46