3

Why can't we compose Function and Consumer just like we compose functions?

Function<Integer, String> a = Object::toString;
Consumer<String> b = x -> System.out.println(x);
Consumer<Integer> composed = a.andThen(b);

This seems like an obvious improvisation to the existing Function interface. Is there any reason why this capability was avoided in Java 8?

Also, is it a common practice to use an extended implementation of Function as follows?

interface FunctionImproved<T, R> extends Function<T, R> {
    default Consumer<T> andThen(Consumer<R> consumer) {
        return x -> consumer.accept(apply(x));
    }
}
Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
ant_1618
  • 1,861
  • 4
  • 17
  • 26

4 Answers4

2

The andThen method on a Function<A0, A1> instance expects a Function<A1, A2>, not a Consumer<A1>:

a.andThen(string -> {
    b.accept(string);
    return string;
});

It has a side-effect since our "consumer" (actually, it's a Function<A1, A1>) will be returning an ignored value from the previous Function<Integer, String> a function. It's not what we want.


There is a different way to compose a Function<A0, A1> and a Consumer<A1>:

Consumer<Integer> c = i -> b.accept(a.apply(i));

which can be generalised to a utility method:

<A0, A1> Consumer<A0> functionAndThenConsumer(Function<A0, A1> f, Consumer<A1> c) {
    return i -> c.accept(f.apply(i));
}

Is it a common practice to use an extended implementation of Function?

There are two options:

  1. Having static methods that do some conversions between functional interfaces (like I did).

  2. Extending these standard functional interfaces with default methods (like you did). Just choose a right meaningful name, not like FunctionImproved.

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
  • 1
    for the utility method ideally you'd want `Function super A0, ? extends A1> f, Consumer c` as the parameters rather than `Function f, Consumer c`. – Ousmane D. Nov 27 '17 at 22:21
  • @Aominè, we can do this only for `A0`, because of `Consumer`, right? – Andrew Tobilko Nov 27 '17 at 22:43
  • nope, we can do it for both as `? extends A1` would definitely satisfy `c.accept(...)`. – Ousmane D. Nov 27 '17 at 22:53
  • `? extends A1` is different for `Consumer` and `Function`, isn't it? – Andrew Tobilko Nov 27 '17 at 23:05
  • 3
    Hmm, `i -> b.accept(a.apply(i))` or `functionAndThenConsumer(a, b)`… Regarding option 2, I don’t see how extending the functional interface helps when you already have two instances of the standard interfaces. And when you don’t have existing instances, there’s even less sense in creating a `FunctionImproved` instance just to call a combining method afterwards, when you can use a readable lambda in the first place. – Holger Nov 28 '17 at 09:44
1

Functions and Consumers serve two different masters (in a sense).

  • A function accepts one (or two in the case of BiFunction) types and produces a third type, which may be either the first or second type.
  • A consumer accepts one (or two in the case of BiConsumer) types, and produces no output.

In both cases, there should be a discrete and single operation being done, which would make it easier to reason about the operation's thread safety.

It looks like you're trying to do a simple mapping of one type to another (String to Integer), then call an operation on it. If your elements were in a Stream, this could be written as:

elements.map(Objects::toString).forEach(System.out::println);

...and if it were in a collection or IntStream, you could use this instead:

elements.forEach(System.out::println);

Ultimately what you're attempting may be a solution in search of a problem. Once you're clearer as to the role that functions and consumers actually play, attempts at compositions like the above become less necessary.

Makoto
  • 104,088
  • 27
  • 192
  • 230
1

Why can't we compose Function and Consumer just like we compose functions?

It's all about Java's type system. The Function.andThen method accepts an argument of type Function (which is not of type Consumer). There's no relation between a Consumer and a Function in Java, i.e. one doesn't extend the other one, so you can't pass a Consumer to Function's andThen method.

There are workarounds available, though. One is with a default method, as you showed, while another approach would be to use a static method, as shown in Andrew's answer.

Here's another way, using a higher-order function:

BiFunction<Function<T, R>, Consumer<R>, Consumer<T>> composer =
    (function, consumer) -> t -> consumer.accept(function.apply(t));

You can use it this way:

Consumer<Integer> composed = composer.apply(Integer::toString, System.out::println);
fps
  • 33,623
  • 8
  • 55
  • 110
-1

There's no reason, why this composition should not exist as part of the Function API. The designers of Java 8 simply did not foresee all possible combinations and use cases: that's why Java 9 added some more convenience methods and probably we'll see more in coming releases.

Regarding the "improved" function, I would not dare to create a new interface just for that, especially considering the choice of the interface name. The suffix "Improved", just like all the "Utils", "Helpers" etc, does not possess self-explaining semantics and should be avoided. Better solution is to create a functional namespace like this:

final class Compositions {
     private Compositions() {}
     public static <T,R> Function<Consumer<R>, Consumer<T>> to(Function<T,R> f) { 
          return consumer -> (value -> consumer.accept(f.apply(value));
     } 
}

import static Compositions.*;
Function<Integer,String> stringValue = Integer::toString;
Consumer<Integer> printInt = to(stringValue).apply(System.out::println);
printInt.accept(1);
Ivan Gammel
  • 652
  • 7
  • 17