Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compose Function<T, R> and Consumer<R> in Java 8?

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));
    }
}
like image 754
ant_1618 Avatar asked Aug 03 '26 20:08

ant_1618


2 Answers

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.

like image 172
Andrew Tobilko Avatar answered Aug 06 '26 09:08

Andrew Tobilko


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.

like image 40
Makoto Avatar answered Aug 06 '26 08:08

Makoto



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!