Mapping an object using a List<UnaryOperator<...>>

805 views Asked by At

I have a List<UnaryOperator<String>> and need to map/transform a String by passing it through the list of operators. I have the following functional Java 11 code:

UnaryOperator<String> addA = (startString) -> startString + "a";
UnaryOperator<String> addB = (startString) -> startString + "b";
UnaryOperator<String> addc = (startString) -> startString + "c";
List<UnaryOperator<String>> operators = List.of(addA, addB, addc);
String concatenatedString =
    operators
        .stream()
        .reduce(
            "", // identity
            (value, op) -> op.apply(value), // accumulator
            (value1, value2) -> value1.concat(value2) // combiner
        );
System.out.println(concatenatedString); // prints "abc" as expected.

The concern I have is the string concatenation is expressed in 2 places. First in each of the UnaryOperators and second in the combiner argument. Makes me wonder if there is a better way to do this?

5

There are 5 answers

0
Nikolas Charalambidis On BEST ANSWER

You can use the advantage that UnaryUperator<T> extends Function<T, T> and chain multiple calls of Function::andThen to get a composed UnaryOperator<String> of all within the list:

UnaryOperator<String> mergedUnaryOperators = operators.stream()
            .reduce((l, r) -> (string) -> l.andThen(r).apply(string))
            .orElseGet(UnaryOperator::identity);

String output = mergedUnaryOperators.apply("");      // results in "abc"

To have a clearer picture how does it work, this is called inside the reduce method:

new BinaryOperator<UnaryOperator<String>>() {
  @Override
  public UnaryOperator<String> apply(UnaryOperator<String> l, UnaryOperator<String> r) {
     return string -> l.andThen(r).apply(string);
  }
}
0
Thiyagu On

You can compose the three UnaryOperators into a single Function using andThen to form a pipeline of transformation.

Function<String, String> addABC = addA
            .andThen(addB)
            .andThen(addc);
System.out.println(addABC.apply("")); //Prints abc

If you have many UnaryOperators to start with, see Nicholas' answer on how to reduce them to a single UnaryOperator.

0
Sweeper On

The reduce method uses the combiner to achieve parallelism. In a parallel stream, reduce can run many of your unary operators at once, and use the provided combiner to combine the results. The existence of combiner is by-design, and it is not at fault.

The problem with this code is not that you have expressed concatenation in two places in the source code, but rather that you are doing string concatenation a lot of times at runtime, which creates a lot of unnecessary strings.

You should use a string builder instead. Since StringBuilder is mutable, you won't be creating a lot of StringBuilders (unless you use parallel streams, but that's another story), and you will be using collect, rather than reduce. Notice the other change from UnaryOperator to Consumer too:

Consumer<StringBuilder> addA = (startString) -> startString.append("a");
Consumer<StringBuilder> addB = (startString) -> startString.append("b");
Consumer<StringBuilder> addc = (startString) -> startString.append("c");
List<Consumer<StringBuilder>> operators = List.of(addA, addB, addc);
StringBuilder concatenatedString =
    operators
        .stream()
        .collect(
            StringBuilder::new, // identity
            (value, op) -> op.accept(value), // accumulator
            StringBuilder::append // combiner
        );
System.out.println(concatenatedString);
0
ArmDuke On

I can suggest another way to concatenate strings using the binary operator, but this proposal is unlikely to be an optimization in a snippet of your code.

String concatenatedString = operators
                .stream().reduce((op1, op2) -> (val) -> op1.apply(val) + op2.apply(val))
                .get().apply("");
0
Michal Václavek On

And what about this solution:

Function<String, String> combineF = operators.stream()
    .reduce(UnaryOperator.identity(), (l, r) -> (string) ->  r.compose(l).apply(string));
System.out.println(combineF.apply(""));