Evolutions of enums and switch statements! ?? Try to achieve algebraic data types and pattern matching in Java

Mikatus Advent Calendar 2019 Day 8 article.

If you read Reactive in practice: A complete guide to event-driven systems development in Java, then The algebraic data type pattern (algebraic) Data type pattern) It was interesting to note that something came out. ..

Example: Representing an order type in a stock trading system

Reactive in practice provides an example of how to represent the types of orders in the stock trading system Stock Trader. As anyone who has traded stocks knows, there are the following types of stock orders:

To represent these orders, a design pattern called the algebraic data type pattern in Reactive in practice is used. Why not an enum (Enum)? Let's unravel the mystery.

Generate an experimental Gradle project

I also want to use Lombok, so experiment with the Gradle project instead of JShell. Let's generate a Gradle project with the following command.

$ mkdir stock-trader
$ cd stock-trader
$ gradle init \
    --type java-library \
    --dsl groovy \
    --test-framework junit \
    --project-name stock-trader \
    --package com.example

Implement the order type of stock trading as an enumeration type

First, let's implement only market orders and limit orders as enumerated types. Since limit orders need to hold a limit price, give the enum a limitPrice field. It also defines a getter getLimitPrice method and a setter setLimitPrice method. ʻIsExecutable` An abstract method that determines whether an order is feasible is defined and overridden by each enumeration value.

src/main/java/com/example/order/OrderType.java


package com.example.order;

public enum OrderType {
    MARKET {
        @Override
        public boolean isExecutable(int currentPrice) {
            return true;
        }
    },
    LIMIT {
        @Override
        public boolean isExecutable(int currentPrice) {
            return currentPrice <= getLimitPrice();
        }
    };

    private int limitPrice;

    public int getLimitPrice() {
        return limitPrice;
    }

    public void setLimitPrice(int limitPrice) {
        this.limitPrice = limitPrice;
    }

    public abstract boolean isExecutable(int currentPrice);
}

Let's write a test that uses this ʻOrderType` enum.

src/test/java/com/example/order/OrderTypeTest.java


package com.example.order;

import org.junit.Test;
import static org.junit.Assert.*;

public class OrderTypeTest {
    @Test
    public void testIsExecutableOnMarketOrder() {
        OrderType orderType = OrderType.MARKET;

        assertTrue(orderType.isExecutable(100));
    }

    @Test
    public void testIsExecutableOnLimitOrder() {
        OrderType orderType = OrderType.LIMIT;
        orderType.setLimitPrice(100);

        assertTrue(orderType.isExecutable(100));
    }
}

As a matter of fact, implementing the order type of stock trading as an enumeration has the following problems.

There may be various solutions to these problems, but this time I will try to solve them using algebraic data type patterns.

Implement the order type of stock trading as an algebraic data type using the shield class pattern

An article called Maybe in Java is referenced in Reactive in practice as a Sealed Class Pattern. Let's implement only market orders and limit orders as algebraic data types in that shield class pattern.

src/main/java/com/example/order/OrderType.java


package com.example.order;

public abstract class OrderType {
    private OrderType() {
    }

    public static final class Market extends OrderType {
        @Override
        public boolean isExecutable(int currentPrice) {
            return true;
        }
    }

    public static final class Limit extends OrderType {
        private int limitPrice;

        public Limit(int limitPrice) {
            this.limitPrice = limitPrice;
        }

        public int getLimitPrice() {
            return limitPrice;
        }

        @Override
        public boolean isExecutable(int currentPrice) {
            return currentPrice <= limitPrice;
        }
    }

    public abstract boolean isExecutable(int currentPrice);
}

Here are some supplements about the ʻOrderType class. By declaring ʻOrderType as an abstract class and defining a private constructor, the classes that can inherit ʻOrderType are limited to the inner classes of ʻOrderType. Also, by declaring the Market and Limit classes as final classes, the Market and Limit classes cannot be inherited. I think this is the reason why it is called the shield class pattern.

Let's write a test that uses this ʻOrderType` class.

src/test/java/com/example/order/OrderTypeTest.java


package com.example.order;

import org.junit.Test;
import static org.junit.Assert.*;

public class OrderTypeTest {
    @Test
    public void testIsExecutableOnMarketOrder() {
        OrderType orderType = new OrderType.Market();

        assertTrue(orderType.isExecutable(100));
    }

    @Test
    public void testIsExecutableOnLimitOrder() {
        OrderType orderType = new OrderType.Limit(100);

        assertTrue(orderType.isExecutable(100));
    }
}

The following problems caused by implementing the order type of stock trading by enumeration type could be solved by implementing it by algebraic data type.

On the other hand, there is a problem that it is the responsibility of the order type to determine whether or not the order can be executed. This is a problem common to the implementations illustrated for enums and algebraic data types, respectively. Whether or not the order can be executed, there may be times when you want to implement conditional branching outside the ʻOrderType` class as shown below.

    @Test
    public void testSwitchOnLimitOrder() {
        OrderType orderType = OrderType.LIMIT;
        orderType.setLimitPrice(100);
        int currentPrice = 100;

        boolean result = false;
        switch (orderType) {
            case MARKET:
                result = true;
                break;
            case LIMIT:
                result = currentPrice <= orderType.getLimitPrice();
                break;
            default:
                throw new UnsupportedOperationException("Unsupported order type");
        }

        assertTrue(result);
    }
    @Test
    public void testIfOnLimitOrder() {
        OrderType orderType = new OrderType.Limit(100);
        int currentPrice = 100;

        boolean result = false;
        if (orderType instanceof OrderType.Market) {
            result = true;
        } else if (orderType instanceof OrderType.Limit) {
            result = currentPrice <= orderType.getLimitPrice();
        } else {
            throw new UnsupportedOperationException("Unsupported order type");
        }

        assertTrue(result);
    }

Such conditional branching has the following problems in common.

To solve this problem, let's introduce pattern matching using the Visitor pattern.

Implement pattern matching using the Visitor pattern

We call it pattern matching for convenience, but don't expect pattern matching like Scala as it only applies the Visitor pattern. Also, the ʻisExecutable` method will be removed to make the code easier to understand.

src/main/java/com/example/order/OrderTypeTest.java


package com.example.order;

public abstract class OrderType {
    private OrderType() {
    }

    public static final class Market extends OrderType {
        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    public static final class Limit extends OrderType {
        private int limitPrice;

        public Limit(int limitPrice) {
            this.limitPrice = limitPrice;
        }

        public int getLimitPrice() {
            return limitPrice;
        }

        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    public interface CaseBlock<T> {
        T _case(Market market);
        T _case(Limit limit);
    }

    public abstract <T> T match(CaseBlock<T> caseBlock);
}

ʻOrderType declares a matchabstract method in the abstract class and implements it in each type. Thematch method receives an instance of a class that implements the CaseBlockinterface and calls its_casemethod. I don't think it's necessary to explain the Visitor pattern again, but the overloading of the_casemethod here causes the process to branch off depending on each type. It's a slapstick, but I named it_case because case` cannot be used as a reserved word.

Let's write a test that uses this ʻOrderType` class. I think it's easier to understand if you look at how to use it.

src/test/java/com/example/order/OrderTypeTest.java


package com.example.order;

import org.junit.Test;
import static org.junit.Assert.*;

public class OrderTypeTest {
    @Test
    public void testPatternMatchingOnLimitOrder() {
        OrderType orderType = new OrderType.Limit(100);
        int currentPrice = 100;

        boolean result = orderType.match(new OrderType.CaseBlock<>() {
            @Override
            public Boolean _case(OrderType.Market market) {
                return true;
            }

            @Override
            public Boolean _case(OrderType.Limit limit) {
                return currentPrice <= limit.getLimitPrice();
            }
        });

        assertTrue(result);
    }
}

This solves the following problems.

If you add a new type to the ʻOrderType` class, of course you have to modify the code. However, as long as the conditional branch is described by the pattern matching as described above, an error will occur at compile time, so that the correction part can be easily identified and the correction omission can be prevented.

Refine algebraic data types with Lombok

Now, it turns out that algebraic data type patterns seem useful. However, it is cumbersome to define and the code is not well visible. To improve that point a little, let's introduce Lombok here.

Add the following line to the build.gradle file.

--- a/build.gradle
+++ b/build.gradle
@@ -9,6 +9,7 @@
 plugins {
     // Apply the java-library plugin to add support for Java Library
     id 'java-library'
+    id "io.freefair.lombok" version "4.1.5"
 }
 
 repositories {

Writing using Lombok looks like this:

src/main/java/com/example/order/OrderTypeTest.java


package com.example.order;

import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.Value;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public abstract class OrderType {
    @Value
    @EqualsAndHashCode(callSuper = false)
    public static class Market extends OrderType {
        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    @Value
    @EqualsAndHashCode(callSuper = false)
    public static class Limit extends OrderType {
        int limitPrice;

        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    public interface CaseBlock<T> {
        T _case(Market market);
        T _case(Limit limit);
    }

    public abstract <T> T match(CaseBlock<T> caseBlock);
}

Implement the definition of private constructor with NoArgsConstructor annotation. The transaction type is annotated with Value as a class that creates a value object. Now both the Market class and the Limit class are final classes. It simplifies the declaration of fields in the Limit class, but all fields are private final, and constructors and getters are automatically generated. Note that the ʻEqualsAndHashCode annotation is added because a warning is issued to specify the ʻEqualsAndHashCode annotation.

Prepare a static constructor

From here on, I think it's a matter of taste, but having a static constructor makes it more like that.

src/main/java/com/example/order/OrderTypeTest.java


package com.example.order;

import lombok.*;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public abstract class OrderType {
    @Value
    @EqualsAndHashCode(callSuper = false)
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    public static class Market extends OrderType {
        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    public static OrderType market() {
        return new Market();
    }

    @Value
    @EqualsAndHashCode(callSuper = false)
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    public static class Limit extends OrderType {
        int limitPrice;

        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    public static OrderType limit(int limitPrice) {
        return new Limit(limitPrice);
    }

    public interface CaseBlock<T> {
        T _case(Market market);
        T _case(Limit limit);
    }

    public abstract <T> T match(CaseBlock<T> caseBlock);
}

Prepare static constructors like the ʻOrderType.market method and the ʻOrderType.limit method. On top of that, the constructors for the Market and Limit classes are made into protected constructors with the ʻAllArgsConstructor` annotation.

The test is also slightly modified.

src/test/java/com/example/order/OrderTypeTest.java


package com.example.order;

import org.junit.Test;
import static org.junit.Assert.*;

public class OrderTypeTest {
    @Test
    public void testPatternMatchingOnLimitOrder() {
        OrderType orderType = OrderType.limit(100);
        int currentPrice = 100;

        boolean result = orderType.match(new OrderType.CaseBlock<>() {
            @Override
            public Boolean _case(OrderType.Market market) {
                return true;
            }

            @Override
            public Boolean _case(OrderType.Limit limit) {
                return currentPrice <= limit.getLimitPrice();
            }
        });

        assertTrue(result);
    }
}

If you do so far, it will be messed up with annotations, so please feel free to use it.

Implement stock trading order types in Scala as algebraic data types

This is easy to do with Scala, so let's try it with the Scala REPL.

scala> :paste 
// Entering paste mode (ctrl-D to finish)

sealed trait OrderType
case object Market extends OrderType
case class Limit(limitPrice: Int) extends OrderType

// Exiting paste mode, now interpreting.

defined trait OrderType
defined object Market
defined class Limit

scala> :paste 
// Entering paste mode (ctrl-D to finish)

val orderType: OrderType = Limit(100)
val currentPrice = 100

orderType match {
  case Market => true
  case Limit(limitPrice) => currentPrice <= limitPrice
}

// Exiting paste mode, now interpreting.

orderType: OrderType = Limit(100)
currentPrice: Int = 100
res3: Boolean = true

It can be written concisely with Scala. Kotlin seems to be concise, but I haven't tried it.


This time, I explained what is called an algebraic data type pattern. Somehow Java is expressive. Also, as Java evolves, it seems that grammars that make it easier to implement algebraic data type patterns will be introduced, so it seems that we will keep an eye on Java in the future. That said, Scala's expressiveness is still fascinating, so I'd love to enjoy both Java and Scala.

References

Reactive in practice: A complete guide to event-driven systems development in Java

Recommended Posts

Evolutions of enums and switch statements! ?? Try to achieve algebraic data types and pattern matching in Java
Try to realize Scala-like Option type pattern matching and map, flatMap in Java
Discrimination of Enums in Java 7 and above
[Java] Types of comments and how to write them
Java conditional branching: How to create and study switch statements
[Java Silver] Things to be aware of regarding switch statements
Java if and switch statements
Read the data of Shizuoka point cloud DB with Java and try to detect the tree height.
[Introduction to Java] Variables and types (variable declaration, initialization, data type)
Basics of Java development ~ How to write programs (variables and types) ~
How to delete large amounts of data in Rails and concerns
Basic data types and reference types (Java)
Try to implement Yubaba in Java
Java basic data types and reference types
List of types added in Java 9
Sample code to serialize and deserialize Java Enum enums and JSON in Jackson
The story of forgetting to close a file in Java and failing
How to change the maximum and maximum number of POST data in Spark
Try to solve Project Euler in Java
Try to implement n-ary addition in Java
[Introduction to Java] Variable declarations and types
Java to C and C to Java in Android Studio
Basics of java basics ② ~ if statement and switch statement ~
I received the data of the journey (diary application) in Java and visualized it # 001
Sample code to get the values of major SQL types in Java + MySQL 8.0