koswag
koswag

Reputation: 183

Multiple function composition

Today I encountered a following Java assignment, and I can't figure out how to get past type erasure.

The task is to create a generic InputConverter class, which takes an input of type T and converts it using chain of multiple functions received as a method argument. It has to support a following notation:

Function<String, List<String>> lambda1 = ...;
Function<List<String>, String> lambda2 = ...;
Function<String, Integer> lambda3 = ...;

String input = ...;

List<String> res1 = new InputConverter(input).convertBy(lambda1);

Integer res2 = new InputConverter(input).convertBy(lambda1, lambda2, lambda3);

This is what I came up with:

import java.util.Arrays;
import java.util.function.Function;

public class InputConverter<T> {
    private final T input;

    public InputConverter(T input) {
        this.input = input;
    }

    public <B> B convertBy(Function<T, ?> first, Function<?, ?>... functions) {
        var res = first.apply(input);

        Function<?, B> composed = Arrays.stream(functions)
            .reduce(Function::andThen)
            .orElse(Function.identity());

        return composed.apply(res);
    }

}

This doesn't work of course, since I can't find a way to determine the return type of the last function.

Notes:

Upvotes: 1

Views: 1218

Answers (2)

koswag
koswag

Reputation: 183

So, according to task's author the correct solution is this:

import java.util.Arrays;
import java.util.function.Function;

public class InputConverter<T> {
    private final T input;

    public InputConverter(T input) {
        this.input = input;
    }

    public <B> B convertBy(Function<T, ?> first, Function... functions) {
        var res = first.apply(input);

        Function<Object, B> composed = Arrays.stream(functions)
            .reduce(Function::andThen)
            .orElse(Function.identity());

        return composed.apply(res);
    }

}

Which is not satisfying to me at all. It allows for using vararg parameters, but using raw, unparameterized Function makes no sense. Nikolas Charalambidis' answer is a much better solution as we preserve return type information and safety.

Upvotes: 1

Nikolas
Nikolas

Reputation: 44496

Problem

You would need to chain the generics for each number of expected functions and chain the generic parameters as on the snippet below with five functions:

public <D> E convertBy(
    Function<T, A> first, Function<A, B> second, Function<B, C> third, 
    Function<C, D> fourth, Function<D, E> fifth) {
    ...
}

However, this is not possible for unknown number of parameters (varargs). There is no such thing as "vargenerics" which would dynamically create and chain the generic parameters as above.


Solution

You can instead treat the InputConverter as a builder instead which returns self with each convertBy call and finally packs a result. This recursive behavior allows indefinite number of calls. Try it out:

public static class InputConverter<T> {

    private final T data;

    public InputConverter(T data) {
        this.data = data;
    }

    public <U> InputConverter<U> convertBy(Function<T, U> function) {
        return new InputConverter<>(function.apply(data));
    }

    public T pack() {
        return data;
    }
}

Pretty neat, isn't it? Let's see the usage on a minimal sample:

// let lambda1 split String by characters and create a List
Function<String, List<String>> lambda1 = str -> Arrays.stream(str.split(""))
                                                      .collect(Collectors.toList());
// let lambda2 get the first item
Function<List<String>, String> lambda2 = list -> list.get(0);

// let lambda3 parse a String into the Integer
Function<String, Integer> lambda3 = Integer::parseInt;

String input = "123";                                   // out sample numeric input

List<String> res1 = new InputConverter<String>(input)   // don't forget the generics
    .convertBy(lambda1)
    .pack();

Integer res2 = new InputConverter<String>(input)
    .convertBy(lambda1)
    .convertBy(lambda2)
    .convertBy(lambda3)
    .pack();

System.out.println(res1);                               // [1, 2, 3]
System.out.println(res2);                               // 1

Upvotes: 3

Related Questions