How to use Javaslang Pattern Matching?

Most of Java developers who work with Java 8 are aware of awesome features such as lambdas, method references and Stream API. Those features help us everyday to write o reduce the amount of code and to increase the robustness. Unfortunately one more feature that Java is still missing is pattern matching. Inspired by Scala language Javaslang library brings pattern matching into Java language. Basically Javaslang is a standalone library written in pure java which helps to write functional code in Java 8. You can read more info  about it on their official website.

In this particular article we are going to cover next topics:

  1. How to add Javaslang to your project
  2. How to use basic pattern matching API
  3. How to use Javaslang annotation preprocessor to use object decomposition in pattern matching

How to add Javaslang to your project

Javaslang has one major prerequisite. You project should have Java 1.8 as a target version for compiler. Javaslang is available as an artifact in the maven central repository. Actually Javaslang has several modules :

  • javaslang-gwt GWT module for Javaslang.
  • javaslang-core  Javaslang control statements, immutable collections etc.
  • javaslang-match Adds compile time support for Javaslang’s structural pattern matching feature.
  • javaslang-test A property check framework for random testing of program properties.
  • javaslang-jackson Jackson datatype module, the standard JSON library for Java.
  • javaslang-render  A rendering library, currently housing tree renderers (ascii and graphviz).

We need to include Javaslang-core dependency which has a dependency on javaslang-match module. I am using maven to my pom.xml is following

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.wordpress.nikitapavlenko</groupId>
    <artifactId>javaslang-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>javaslang-demo</name>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.javaslang</groupId>
            <artifactId>javaslang</artifactId>
            <version>2.1.0-alpha</version>
        </dependency>
    </dependencies>

</project>

How to use basic pattern matching API

Lets have a look at very basic and simple example.

import static javaslang.API.*;
...
int i = 1;
String s = Match(i).of(
    Case($(1), "one"),
    Case($(2), "two"),
    Case($(), "?")
);

First of all match is implemented like an expression which means that result of match  can be assigned to a variable. Example above does not need an explanation about what it does, but rather how it works.

So class javaslang.API has static method Match, it creates an instance of Match class. Match has only 2 methods:

...
public final  R of(Case... cases)
public final  Option option(Case... cases)
..

So the depending on what you expect to get you can use method “of” and when no cases match you get MatchError or if you expect no result you can use method “option” and empty option is returned when nothing matches.

Both method accept vararg of Case objects. To create Case object javaslang has static factory methods in javaslang.API. Case method is overloaded and the main idea is that case should contain 2 parts:

  • part that can do matching logic. It can be instance of Predicate or Pattern.
  • part that says what should be returned when value matches. It can be any object or a supplier that will generate object. In example above plain object is used.

In our example we are sending $(1) as matching object, and “one” as returned value. Javaslang has some predefined methods to simplify pattern matching:

  • $() matches everything
  • $(value) incoming value should be equal to given one
  • $(predicate) to match predicate.test(incomingObject) should return true

$() and $(value) are presented in the example above, so lets focus on $(predicate).

import static javaslang.API.*;
...
Predicate<Integer> isEven = i -> i % 2 == 0;
Predicate<Integer> isOdd = isEven.negate();

int i = 1;
String s = Match(i).of(
        Case($(isOdd), "odd"),
        Case($(isEven), String::valueOf)
);

So in the example above you see that when predicate isOdd returns true we return “odd” string, when isEven returns true we are generating string value from number.

How to use Javaslang annotation preprocessor to use object decomposition in pattern matching

Matching object by equals or by pattern is already an awesome feature. But there are many situations when we need to do some matching based on object hierarchies.

So lets imagine that we are developing application in e-commerce domain where we have Order and PaymentTransaction. More details in code sample:

public class Order {

    private String code;
    private OrderStatus status;
    private PaymentTransaction paymentTransaction;
    private BigDecimal total;

   //getters, setters
}

public enum OrderStatus {
    PLACED,
    IN_PROGRESS,
    COMPLETED,
    CANCELLED
}

public class PaymentTransaction {
    private final BigDecimal value;
    private final PaymentType paymentType;
   //getters, setters
}

public enum PaymentType {
    CREDIT_CARD,
    CASH,
    GIFT_CARD,
    PAYPAL,
    RETURN
}

Then lets say you were asked to implement following feature:


Return payment status to customer depending of order state:

  • when order has no payment set – return ‘Not paid yet’
  • when order’s status is Cancelled and payment transaction type is RETURN – then return “Payment cancelled”
  • when order’ status is Completed and payment transaction type is CREDIT_CARD, PAYPAL or GIFT_CARD and payment transaction value equals order’s total – then return “Paid online with ${PaymentType}”
  • when order’ status is Completed and payment transaction type is CASH and payment transaction value equals order’s total – then return “Paid offline with CASH”
  • in all other cases – return empty string

 

We have following test for this:

So lets try to implement this feature using Java 8:

public String getPaymentStatus_javaVersion(Order order) {
        if (order.getPaymentTransaction() == null) {
            return "Not paid yet";
        }
        if (order.getStatus() == OrderStatus.CANCELLED && order.getPaymentTransaction().getPaymentType() == PaymentType.RETURN) {
            return "Payment cancelled";
        }
        if (order.getStatus() == OrderStatus.COMPLETED) {
            List<PaymentType> onlineTypes = Arrays.asList(PaymentType.CREDIT_CARD, PaymentType.PAYPAL, PaymentType.GIFT_CARD);
            if (order.getTotal().compareTo(order.getPaymentTransaction().getValue()) == 0) {
                PaymentType paymentType = order.getPaymentTransaction().getPaymentType();
                if (onlineTypes.contains(paymentType)) {
                    return "Paid online with " + paymentType;
                } else if (paymentType == PaymentType.CASH) {
                    return "Paid offline with CASH";
                }
            }
        }
        return "";
    }

Java version has nested blocks and it seems like something can be simplified. The way of accessing and checking fields can be done in a more declarative way by decomposing object using pattern match.

To use this feature in Javaslang you need to follow several steps:

  1. Define class marked with @Patterns annotation
  2. Implement method to convert your object to TupleN and mark with @Unapply annotation
  3. Compile your project to activate javaslang annotation processor
  4. Use generated sources in Match-Case expression

Source code:

@Patterns
public class Order {

    // fields and getters/setters

    @Unapply
    static Tuple3<String, OrderStatus, PaymentTransaction> Order(Order order) {
        return Tuple.of(order.code, order.status, order.paymentTransaction);
    }
}

@Patterns
public class PaymentTransaction {
    // fields and getters/setters
   @Unapply
    static Tuple2<BigDecimal, PaymentType> PaymentTransaction(PaymentTransaction paymentTransaction) {
        return Tuple.of(paymentTransaction.value, paymentTransaction.paymentType);
    }

}

After building our maven project, on compilation phase javaslang.match.PatternsProcessor generates new java classes. A generated Java class has following name ${InitialName}Patterns. So we have 2 new classes OrderPatterns and PaymentTransactionPatterns.


public final class OrderPatterns {

    private OrderPatterns() {
    }

    public static <_1 extends String, _2 extends OrderStatus, _3 extends PaymentTransaction> Pattern3<Order, _1, _2, _3> Order(Pattern<_1, ?> p1, Pattern<_2, ?> p2, Pattern<_3, ?> p3) {
        return Pattern3.of(Order.class, p1, p2, p3, com.mykytapavlenko.blog.Order::Order);
    }

}

public final class PaymentTransactionPatterns {

    private PaymentTransactionPatterns() {
    }

    public static <_1 extends BigDecimal, _2 extends PaymentType> Pattern2<PaymentTransaction, _1, _2> PaymentTransaction(Pattern<_1, ?> p1, Pattern<_2, ?> p2) {
        return Pattern2.of(PaymentTransaction.class, p1, p2, com.mykytapavlenko.blog.PaymentTransaction::PaymentTransaction);
    }

}

So now we can use Object decomposition in our business logic.

    public String getPaymentStatus_javaslangVersion(Order order) {
        return Match(order).option(
            Case(Order($(), $(OrderStatus.COMPLETED), PaymentTransaction($(eqTotal(order)), $(isIn(PaymentType.CREDIT_CARD, PaymentType.PAYPAL, PaymentType.GIFT_CARD)))),
                    (o, s, t) -> "Paid online with " + t.getPaymentType()),
            Case(Order($(), $(OrderStatus.COMPLETED), PaymentTransaction($(eqTotal(order)), $(isIn(PaymentType.CASH)))), "Paid offline with CASH"),
            Case(Order($(), $(OrderStatus.CANCELLED), PaymentTransaction($(), $(PaymentType.RETURN))), "Payment cancelled"),
            Case(Order($(), $(), $(isNull())), "Not paid yet")).getOrElse("");
    }

    private static Predicate<BigDecimal> eqTotal(Order order) {
        return (paidValue) -> paidValue.compareTo(order.getTotal()) == 0;
    }

In Javaslang version 2.1.0-alpha object decomposition is done in quite straightforward way. When we define Unapply method we describe a way of transforming our domain object to a tuple of n elements.

    @Unapply
    static Tuple3<String, OrderStatus, PaymentTransaction> Order(Order order) {
        return Tuple.of(order.code, order.status, order.paymentTransaction);
    }

Javaslang match library using this unapply creates decomposition method with the same name as our unapply method.

static <_1 extends String, _2 extends OrderStatus, _3 extends PaymentTransaction> Pattern3<Order, _1, _2, _3> Order(Pattern<_1, ?> p1, Pattern<_2, ?> p2, Pattern<_3, ?> p3) {
        return Pattern3.of(Order.class, p1, p2, p3, Order::Order);
    }

The decomposition method accepts as parameters patterns for each element of tuple. It means that for order we can define matching pattern for code, order status and payment transaction. This decomposition method merges 3 different patterns for each field into a single Pattern3 which then is passed to Case method for further checking.

Summary

I hope this article helped you to understand how pattern matching in javaslang works and how it can help in writing more clear, functional and declarative code using Java 8. I believe that javaslang will live long and that next releases will bring java developers more such features.

All source code you can find on my github javaslang-pattern-matching-demo . Thanks for reading!

Advertisements