12 March 2019
CATEGORY
FunctionalJ.io
TAGS
FunctionalJ.ioSum Type

Choice types in Java with FunctionalJ.io

A "Tagged Union" implementation in Java

Introduction

A choice type allows us to specify, in an ad-hoc fashion, what values can be used (see tagged union). It is similar to enum but it also allows the payload to parameterize its values. With choice types, it is possible and easier to design the data type where invalid values are not representable. This article discusses how FunctionalJ.io's @Choice annotation can be used to create choice types in Java.

Background

In essence, a type is a set of possible values. Boolean means only true or false can be used. Integer means only whole number between 2^32 to (-2^32)+1 can be used. Types may have other functions, like describing how to construct the data in memory and what we can do with it, but at the essence, a type defined what values are allowed. In statically-typed compiled languages, this constraint is enforced at the compiled time which, as we all (Java programmers) know, contribute greatly to the ensuring of correctness and reliability of the program. It also makes code much more comprehensible.

Kinds of types we can use are fundamentally tied to a programming language's type system, thus, we are at the mercy of the programming language in term of how precisely we can specify what possible values can be in a given type. Unfortunately for us, Java type system is limited in some aspects when it comes to this. Narrowing or widening primitive types comes to mind. Another is the ability to allow certain values in an ad-hoc fashion. Perhaps, the intent is to use classes to simulate all those and to be fair that can be done, just, with much more code and great deal of care are required. And this is where Choice type comes in.

@Choice types

Choice data types are a type of data that can be in any of the listed values. Choice types are known in other languages as "sum types", "or types", "discriminated unions", "tagged unions", "variant" and others (see on WikiPedia).

To define a choice type, you annotate an interface with @Choice. The interface name must be suffixed by either Spec or Model.

package pkg;

@Choice
interface UpOrDownSpec {
    void Up();
    void Down();
}
This will tell FunctionalJ.io to generate a class called UpOrDown in the same package with the spec class (package pkg). Noted that, since spec interfaces have no actual use but only to specify how the choice type is generated, the spec interfaces do not have to be public and can be put inside any other class/interface.

As said, the code above creates an abstract class called pkg.UpOrDown. This class has two subclasses Up and Down. As because those subclasses have no payload, instances of those sub classes are also created as up and down.

UpOrDown direction1 = UpOrDown.up;
UpOrDown direction2 = UpOrDown.down;

At first glance, it looks just like an enum, but the choice can contain a payload. For example:

@Choice
interface LoginStatusSpec {
    void Login(String userName);
    void Logout();
}

In this case, the Login status can be in two states: Login with a user name and logout.

LoginStatus status1 = LoginStatus.Login("root");
LoginStatus status2 = LoginStatus.Logout();

Understand Choice Types

Choice types are implemented using sealed-type pattern (Thanks to Scala's case classes). The generated class is made abstract with a private constructor. All the choices are generated as inner classes that implement the main choice class.

Checking Choice

isXXX() methods can be used to check what choice a choice object is.

LoginStatus status1 = LoginStatus.Login("root");
LoginStatus status2 = LoginStatus.Logout();
assertTrue(status1.isLogin());
assertFalse(status1.isLogout());
assertFalse(status2.isLogin());
assertTrue(status2.isLogout());

Using Choice

asXXX() methods return a Result<XXX> containing the choice if the type match otherwise returns a result of null.

LoginStatus status1 = LoginStatus.Login("root");
LoginStatus status2 = LoginStatus.Logout();
assertEquals("Login(root)", status1.asLogin().map(String::valueOf).orElse("Not login"));
assertEquals("Not login", status2.asLogin().map(String::valueOf).orElse("Not login"));

Another way to use the choice is the methods ifXXX(...) which has two overloads. These methods run the given code (either a consumer or a runnable) if the type choice type match.

LoginStatus status = LoginStatus.Login("root");
status
    .ifLogin(s -> System.out.println("user: " + s.userName()))
    .ifLogout(()->System.out.println("user: guess"));
// This code will print out "user: root".

Pattern matching

Pattern matching is a preferred way to use choice value as it ensures all the possible values are handled. Another word, pattern matching is exhaustive.

Here is a basic example.

String currentUser = status.match()
    .login (s -> "User: " + s.userName()) 
    .logout("Guess");

In the above code, we pattern match the status. If the status is login, the match returns the string "User: " and the user name. If the status is logout, the match returns the string "Guess".

As mentioned, pattern matching is exhaustive so both the choices must be there for this to compile. The cases must also be in the right order -- this is actually because of the current implementation.

Additional methods

Additional methods can be added to a choice type. To do that, the spec interface must be made static so that the generated class can call to the spec class. Additionally, due to how type is visible at each stage of the compilation, the added methods must use an auxiliary type Self and its generic fiends to indicate this. Consider the following code example:

@Choice
public static interface LinkedListSpec {
    void Nill();
    void Node(Object value, LinkedList rest);
    
    default int length(Self self) {
        LinkedList list = self.unwrap();
        return Match(list)
                .nill(l -> 0)
                .node(l -> 1 + length(l.rest()));
    }
}
...
@Test
public void testLength() {
    assertEquals(0, Nill().length());
    assertEquals(1, Node(5, Nill()).length());
    assertEquals(2, Node(5, Node(6, Nill())).length());
}

This example shows the method length is added to the LinkedList choice type. Notice that the declared method takes Self as a parameter. This self signals the generator that this method will become the generated class method and it will become an instance method. The instance of LinkedList will be wrapped in the self object. The method Self.unwrap(...) is used to unwrap the object. Note: I know this isn't pretty but it is unfortunate that the compilers didn't provide the name of the types that do not exist at the compilation time (only Eclipse compiler does).

Here is another example of how to use Self. This code shows how to use Self with generic by using Self1<T> (generic with one type parameter). It also shows how to use Self1.wrap(...) to wrap the choice-type object for return.

@Choice
public interface OptionSpec<T> {
    void None();
    void Some(T value);
    
    static <T> Self1<T> of(T value) {
        return Self1.wrap(
                (value == null)
                ? Option.None()
                : Option.Some(value));
    }
    default <R> Self1<R> map(Self1<T> self, Function<? super T, ? extends R> mapper) {
        Option<T> option = self.asMe();
        return Self1.wrap(
                Option.of(
                    Match(option)
                    .none(__ -> (R)null)
                    .some((Option.Some<T> some) -> mapper.apply(some.value()))));
    }
}

Java 12's Switch Expressions

Choice types are implemented using the sealed-type pattern (type hierarchy with fix members). The choice type (e.g., Option) is created as the superclass of its case (e.g., None and Some). The super class's constructor is made private and the case classes are made final so they cannot be extended. As subclasses, Java 12's switch expression can be used to check the cases of the choice type.

Example usages of choice types

Since tagged union has been around for a very long time, there are many usages of them. Here are some of that examples:

Basic data structures

@Choice
interface MayBeSpec {
    void Just(T data);
    void Nothing();
}

@Choice
interface EitherSpec {
    void Left(TR rightData);
    void Right(TL leftData);
}

@Choice
interface TrySpec {
    void Success(@Nullable T data);
    void Problem(Exception problem);
}

@Choice
interface LinkedListSpec {
    void Nill();
    void Node(Object value, @Nullable LinkedList rest);
}

States

@Choice
interface LoginStatusSpec {
    void Login(String userName);
    void Logout();
}

Return Values

@Choice
interface ReqestResultSpec {
    void Success(T data);
    void Error(String errorMessage);
    void ConnectionFailed(Exception problem);
}

Commands or Events

@Choice
interface CommandSpec {
    void Forward(int distance);
    void Backward(int distance);
    void Turn(int angle);
    void Explode();
}

Unit of Measure

@Choice
static interface TemperatureSpec {
    void Celsius(double celsius);
    void Fahrenheit(double fahrenheit);
    
    default Temperature.Fahrenheit toFahrenheit(Self self) {
        Temperature temp = self.asMe();
        return temp.match()
                .celsius   (c -> Temperature.Fahrenheit(c.celsius()*1.8 + 32.0))
                .fahrenheit(f -> f);
    }
    default Temperature.Celsius toCelsius(Self self) {
        Temperature temp = self.asMe();
        return temp.match()
                .celsius   (c -> c)
                .fahrenheit(f -> Temperature.Celsius((f.fahrenheit() - 32.0)/1.8));
    }
}

Conclusion

FunctionalJ.io's @Choice brings tagged union to Java. The easiest way to understand choice types is that they are enums with payloads. Choice types also come with exhaustive pattern matching. Additional methods can be added to the choice types. Choice types can be used for many situations and they can be used to create the types that specifically target exact set of valid values and make the invalid ones unrepresentable. I hope this with FunctionalJ.io's @Choice, you can make use of many of the functional program ideas in Java.

Happy coding!
Nawa Man

Note: To use @Choice with Eclipse, you may want to look at this article to set it up.

Comments

Thank you for keeping the comment section positive, constructive and respectful environment. I do appreciate constructive criticism & respectful disagreement! I have ZERO tolerant for disrespect, harassment, threats, cyber-bullying, racism, sexism attacks, foul language, and spam. Comments will be actively moderated and abusive users will be blocked. Keep it civil! :-)