Apply a List of Custom Functions on the given Object sequentially

Viewed 47

Let's say I have an abstract class TransformationFunction and some concrete classes extending it:

public abstract class TransformationFunction {
    public abstract String apply(String input);
}

public class Upper extends TransformationFunction {
    public String apply(String input) {
        return input.toUpperCase();
    }
}

public class Lower extends TransformationFunction {
    public String apply(String input) {
        return input.toLowerCase();
    }
}

I want to apply a list of such functions on a String object and get a final value.

My current implementation:

{ 
    List<TransformationFunction> functions = ...
    T value = ...
    for (TransformationFunction function : functions) {
        value = function.apply(value);
    }
    return value;
}

How do I write this in the functional style?

1 Answers

TL;DR

It can be done using java.util.function.Function having the same input type and result type. You can define a bunch of such functions, and fold them in a single composed function using Stream API:

var result = functions.stream()
    .reduce(Function.identity(), Function::andThen)
    .apply(t);

Reinventing the Wheel

From an Abstract class to a Functional Interface

Firstly, there's no reason for TransformationFunction to be an abstract class(it has no properties to offer to extending classes, and can be substituted with an interface). Always favor interfaces over abstract classes, it makes code loosely coupled. As well as, there's no need for a swarm of subclasses, because we are going to do functional programming.

Secondly, to use functional features in Java, you need a Functional interface, which is an interface having a single abstract method. A lambda expression or a method reference can't conform to an abstract class, they can be used only as an implementation of a Functional interface.

Let's reimplement TransformationFunction as an interface. And let's make it generic.

public interface TransformationFunction<T> {

    T apply(T input);
    
    default TransformationFunction<T> andThen(TransformationFunction<T> after) {
        
        return t -> after.apply(this.apply(t));
    }
}

So basically we are reinventing UnaryOperator interface from the JDK.

I've added default method andThen(), which allow combining two transformation functions.

We can make the interface leaner by extending UnaryOperator, there would be no need in declaring apply() method, but andThen() should stay on its place because the version declared by Function (and inherited by UnaryOperator) differs in types that is consumes and returns and would not be suitable while working with TransformationFunction.

public interface TransformationFunction<T> extends UnaryOperator<T> {
    
    default TransformationFunction<T> andThen(TransformationFunction<T> after) {
        
        return t -> after.apply(this.apply(t));
    }
}

And that's how we can aggregate a collection of transformation functions and apply the given value:

public static <T> T applyAll(Collection<TransformationFunction<T>> functions, T t) {
    
    return functions.stream()
        .reduce(TransformationFunction::andThen)
        .orElse(e -> e)
        .apply(t);
}

main()

public static void main(String[] args) {
    List<TransformationFunction<String>> functions = List.of(
        String::toUpperCase,
        String::strip,
        str -> str.replaceAll("\\d", "")
    );
    
    String value = applyAll(functions, "   abc967defgh");
    System.out.println(value);
}

Output:

ABCDEFGH

java.util.function.Function

The same can be achieved by using standard JDK interface Function<T,R> having the same input and result types (note that method below would not work with UnaryOperator because it derives andThen() implementation from the Function, and we will run into issues with type inference).

public static <T> T applyAll(Collection<Function<T, T>> functions, T t) {
    
    return functions.stream()
        .reduce(Function.identity(), Function::andThen)
        .apply(t);
}

main()

public static void main(String[] args) {
    List<Function<String, String>> functions = List.of(
        String::toUpperCase,
        String::strip,
        str -> str.replaceAll("\\d", "")
    );
    
    String value = applyAll(functions, "   abc967defgh");
    System.out.println(value);
}

Output:

ABCDEFGH
Related