Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Introducing Combinators (Part 2)

DZone's Guide to

Introducing Combinators (Part 2)

As we continue our journey through combinators, we focus on the Provided, Precondition, or Postcondition combinators and see them in action.

· Java Zone ·
Free Resource

Microservices. Streaming data. Event Sourcing and CQRS. Concurrency, routing, self-healing, persistence, clustering...learn how Akka enables Java developers to do all this out of the box! Brought to you in partnership with Lightbend.

This issue of The Bounds of Java Newsletter is the second part of a series about combinators. Here we introduce additional combinators and show how they can be implemented by building on some combinators presented in the previous newsletter.

More Combinators

In the previous part of this series, I introduced the Before, After, and Around combinators. Let's go on and show more combinators and how we can use them.

Disclaimer: The combinators introduced below are inspired and based on the excellent Method Combinators library written by Reginald «raganwald» Braithwaite. Please check his page and work for further reference.

Provided

This is one of the most useful combinators I've ever seen. Look at the create method carefully:

@FunctionalInterface
public interface Provided<T, R>
        extends Function<Predicate<T>,
                Function<Function<T, R>,
                Function<Function<T, R>,
                Function<T, R>>>> {

    static <T, R> Provided<T, R> create() {
        return condition -> function -> fallback ->
            arg -> (condition.test(arg) ? function : fallback).apply(arg);
    }

    static <T, R> Function<T, R> decorate(
            Predicate<T> condition,
            Function<T, R> function,
            Function<T, R> fallback) {
        return Provided.<T, R>create().apply(condition).apply(function).apply(fallback);
    }
}


This combinator receives the condition predicate, the original function to be decorated and a fallback function, and returns a decorated function. The argument and return types of all function, fallback, and the result function match, while the type of the condition predicate's argument matches the type of the original function's argument.

It works by choosing either the original or the fallback function, based on the result of the condition. First, the condition predicate is tested against the arg argument, which is the argument of the original function. Then, if the test on condition returns true, the original function is selected, whereas if the test returns false, the fallback function is selected. Thus, the result function is a function that selects one function out of two based on a condition, and applies its argument to the selected function.

The following example shows how to use the Provided combinator:

class ProvidedExample {

    void demo() {
        System.out.println("------------------------------------");
        System.out.println("Starting PROVIDED combinator demo...");
        System.out.println("------------------------------------");

        Function<BigDecimal, String> addTaxDecorated =
                Provided.decorate(this::isTaxable, this::addTax, this::fallback);

        String result1 = addTaxDecorated.apply(new BigDecimal("100"));

        System.out.println("Done - Result is " + result1);
        System.out.println("------------------------------------");

        String result2 = addTaxDecorated.apply(new BigDecimal("5"));

        System.out.println("Done - Result is " + result2);
        System.out.println("------------------------------------");
    }

    private boolean isTaxable(BigDecimal argument) {
        boolean condition = argument.compareTo(BigDecimal.TEN) >= 0; // argument >= 10
        System.out.println("PROVIDED: Argument is " + argument + ", condition is " + condition);
        return condition;
    }

    private String addTax(BigDecimal amount) {
        System.out.println("Adding heavy taxes to poor citizen...");
        return "$" + amount.multiply(new BigDecimal("1.22"));
    }

    private String fallback(BigDecimal amount) {
        System.out.println("Fallback: tax exemption");
        return "$" + amount;
    }
}


Here this::addTax represents our original function and this::isTaxable acts as our condition predicate, while this::fallback is the function that will be executed if the condition is not satisfied. The logic is to execute the original function if the argument of the decorated function is greater than or equal to 10, otherwise execute the fallback function. Here's the output:

------------------------------------
Starting PROVIDED combinator demo...
------------------------------------
PROVIDED: Argument is 100, condition is true
Adding heavy taxes to poor citizen...
Done - Result is $122.00
------------------------------------
PROVIDED: Argument is 5, condition is false
Fallback: tax exemption
Done - Result is $5
------------------------------------


This output shows that, when called with an argument of 100, a terrifying 22% tax is applied, while, when called with an argument of 5, the amount remains tax free.

The Provided combinator can be used almost everywhere... Any time the execution of some code depends on the successful evaluation of a condition, it is a good opportunity to use this combinator.

Precondition

This combinator is very useful too. It can be seen as a variant of the Provided combinator, in which we throw an exception instead of executing a fallback function. Here's the code:

@FunctionalInterface
public interface Precondition<T, R, X extends RuntimeException>
        extends Function<Predicate<T>,
                Function<Function<T, R>,
                Function<Function<T, X>,
                Function<T, R>>>> {

    static <T, R, X extends RuntimeException> Precondition<T, R, X> create() {
        return condition -> function -> error -> Provided.decorate(
            condition,
            function,
            arg -> {
                throw error.apply(arg);
            });
    }

    static <T, R, X extends RuntimeException> Function<T, R> decorate(
            Predicate<T> condition,
            Function<T, R> function,
            Function<T, X> error) {
        return Precondition.<T, R, X>create().apply(condition).apply(function).apply(error);
    }
}


The implementation of this combinator delegates to the Provided combinator's decorate method. It receives the condition predicate, the original function to be decorated and an error function, and returns a decorated function. The function that is passed as a fallback to the Provided combinator is implemented by throwing the exception returned by the error function. The logic is the one of the Provided combinator: If the condition predicate is not satisfied, the fallback function will be executed, i.e. the exception returned by the error function will be thrown.

Take a look at the following example to see how useful the Precondition combinator can be:

class PreconditionExample {

    void demo() {
        System.out.println("----------------------------------------");
        System.out.println("Starting PRECONDITION combinator demo...");
        System.out.println("----------------------------------------");

        Function<BigDecimal, String> addTaxDecorated = Precondition.decorate(
                this::isGreaterThanZero,
                this::addTax,
                NonPositiveAmountTaxException::new);

        String result1 = addTaxDecorated.apply(new BigDecimal("10"));

        System.out.println("Done - Result is " + result1);
        System.out.println("----------------------------------------");

        try {
            String result2 = addTaxDecorated.apply(new BigDecimal("-5"));
            System.out.println("Done - Result is " + result2);

        } catch (NonPositiveAmountTaxException e) {

            System.out.println("Exception: " + e.getMessage());
        }
        System.out.println("----------------------------------------");
    }

    private boolean isGreaterThanZero(BigDecimal argument) {
        boolean condition = argument.compareTo(BigDecimal.ZERO) > 0; // argument > 0
        System.out.println("PRECONDITION: Argument is " + argument + ", condition is " + condition);
        return condition;
    }

    private String addTax(BigDecimal amount) {
        System.out.println("Adding heavy taxes to poor citizen...");
        return "$" + amount.multiply(new BigDecimal("1.22"));
    }
}

public class NonPositiveAmountTaxException
        extends RuntimeException {

    private NonPositiveAmountTaxException(String message) {
        super(message);
    }

    public NonPositiveAmountTaxException(BigDecimal amount) {
        this("Amount to be taxed must be > 0 but was " + amount);
    }
}


Again, this::addTax represents our original function. The predicate condition, though, is given by the this::isGreaterThanZero method, while NonPositiveAmountTaxException::new is a constrcutor reference that acts as the error function (note that the public constructor of NonPositiveAmountTaxException expects an argument of the same type as the original function). The logic is to execute the original function if the argument of the decorated function is greater than 0, otherwise throw NonPositiveAmountTaxException. Here's the output:

----------------------------------------
Starting PRECONDITION combinator demo...
----------------------------------------
PRECONDITION: Argument is 10, condition is true
Adding heavy taxes to poor citizen...
Done - Result is $12.20
----------------------------------------
PRECONDITION: Argument is -5, condition is false
Exception: Amount to be taxed must be > 0 but was -5
----------------------------------------


This output shows that, when called with an argument of 10, a 22% tax is applied, while, when called with an argument of -5, a NonPositiveAmountTaxException is thrown.

The Precondition combinator can be used whenever a runtime exception must be thrown if a condition is not met before executing some code.

Postcondition

If there exists a Precondition combinator, there must exist a Postcondition one. Let me introduce it to you:

@FunctionalInterface
public interface Postcondition<T, R, X extends RuntimeException>
        extends Function<Function<T, R>,
                Function<BiPredicate<T, R>,
                Function<BiFunction<T, R, X>,
                Function<T, R>>>> {

    static <T, R, X extends RuntimeException> Postcondition<T, R, X> create() {
        return function -> condition -> error -> After.decorate(
                function,
                (argument, result) -> {
                    if (!condition.test(argument, result)) {
                        throw error.apply(argument, result);
                    }
                });
    }

    static <T, R, X extends RuntimeException> Function<T, R> decorate(
            Function<T, R> function,
            BiPredicate<T, R> condition,
            BiFunction<T, R, X> error) {
        return Postcondition.<T, R, X>create().apply(function).apply(condition).apply(error);
    }
}


This combinator is implemented by means of delegation to the After combinator's decorate method. It receives the original function to be decorated, a condition predicate and an error function, and returns a decorated function. After the original function is executed, a BiFunction that receives both the original function's argument and its result is executed. This BiFunction checks the condition BiPredicate and throws the exception returned by the error function if the condition hasn't been met.

Here's an example that shows how the Postcondition combinator can be used:

class PostconditionExample {

    void demo() {
        System.out.println("-----------------------------------------");
        System.out.println("Starting POSTCONDITION combinator demo...");
        System.out.println("-----------------------------------------");

        Function<BigDecimal, String> addTaxDecorated1 = Postcondition.decorate(
                this::addTax,
                this::checkResultStartsWith$,
                InvalidTaxResultFormatException::new);

        String result1 = addTaxDecorated1.apply(new BigDecimal("10"));

        System.out.println("Done - Result is " + result1);
        System.out.println("-----------------------------------------");

        try {
            Function<BigDecimal, String> addTaxDecorated2 = Postcondition.decorate(
                    this::addTaxIncorrect,
                    this::checkResultStartsWith$,
                    InvalidTaxResultFormatException::new);

            String result2 = addTaxDecorated2.apply(new BigDecimal("10"));
            System.out.println("Done - Result is " + result2);

        } catch (InvalidTaxResultFormatException e) {

            System.out.println("Exception: " + e.getMessage());
        }
        System.out.println("-----------------------------------------");
    }

    private String addTax(BigDecimal amount) {
        System.out.println("Adding heavy taxes to poor citizen...");
        return "$" + amount.multiply(new BigDecimal("1.22"));
    }

    private String addTaxIncorrect(BigDecimal amount) {
        System.out.println("Incorrectly adding heavy taxes to poor citizen...");
        return "€" + amount.multiply(new BigDecimal("1.22"));
    }

    private boolean checkResultStartsWith$(BigDecimal argument, String result) {
        boolean condition = result.startsWith("$");
        System.out.println("POSTCONDITION: Argument is " + argument + 
                ", result is " + result + ", condition is " + condition);
        return condition;
    }
}

public class InvalidTaxResultFormatException
        extends RuntimeException {

    private InvalidTaxResultFormatException(String message) {
        super(message);
    }

    public InvalidTaxResultFormatException(BigDecimal amount, String formatted) {
        this("Result of adding tax to amount " + amount + 
                " has incorrect format: " + formatted);
    }
}


The this::addTax method reference again represents our original function, which in this case returns a correctly formatted result. We're using the this::addTaxIncorrect method reference as an example of function that doesn't satisfy our postcondition. Precisely, the predicate for this postcondition is given by the this::checkResultStartsWith$ method reference, while InvalidTaxResultFormatException::new is a constructor reference that acts as the error function (note that the public constructor of InvalidTaxResultFormatException expects arguments that match the original function's argument and result types). The logic is to execute the original function and then check if its result starts with the $ sign. If it doesn't, an InvalidTaxResultFormatException is thrown. Here's the output:

-----------------------------------------
Starting POSTCONDITION combinator demo...
-----------------------------------------
Adding heavy taxes to poor citizen...
POSTCONDITION: Argument is 10, result is $12.20, condition is true
Done - Result is $12.20
-----------------------------------------
Incorrectly adding heavy taxes to poor citizen...
POSTCONDITION: Argument is 10, result is €12.20, condition is false
Exception: Result of adding tax to amount 10 has incorrect format: €12.20
-----------------------------------------


This output shows that, when the result of applying taxes to our poor citizen starts with the $ sign, everything works as expected, while, when it starts with the sign, an InvalidTaxResultFormatException is thrown.

The Postcondition combinator comes in very handy when writing tests, though it might also be used for testing conditions that must hold true after executing some logic.

Conclusion

This was just a brief and very informal introduction to combinators. In fully functional programming languages, they are extremely important to control the execution flow of applications. Although as of today they're not essential in Java, it's good to know about them, as the trend is to go more functional over time.

All the code and examples shown here are available in its own GitHub repo. Please contact me if you find something to correct or improve.

Microservices. Streaming data. Event Sourcing and CQRS. Concurrency, routing, self-healing, persistence, clustering...learn how Akka enables Java developers to do all this out of the box! Brought to you in partnership with Lightbend. 

Topics:
java ,functional programming ,combinators ,java 8 ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}