Skip to content
Daniel Thilo Schroeder edited this page Jan 27, 2020 · 2 revisions

adt4j - Algebraic Data Types for Java

Tutorial

Records/tuples

ADT4J allows to define record/tuple types. This is not the brightest side of adt4j. ADT4J provides functionality similar to immutables.org or google-auto/AutoValue. If you only need record types you may consider using any of these projects. Still adt4j may be a good choice as it allows you transparently switch to more flexible algebraic data types.

We will use record types to introduce some basic adt4j features because record types are familiar to most people.

With adt4j you can easily define record type like this:

    @GenerateValueClassForVisitor(className = "User")
    @Visitor(resultVariableName="R")
    interface UserVisitor<R> {
       R valueOf(String name, int age);
    }

As a result new class User will be mechanically generated by adt4j.

You can leave className parameter of @GenerateValueClassForVisitor-annotation out. Generated class name is chosen by removing Visitor-suffix from visitor-interface name. If visitor-interface name doesn't end with Visitor, Value-suffix is appended to visitor-interface name to form generated class name.

    @GenerateValueClassForVisitor
    @Visitor(resultVariableName="R")
    interface UserVisitor<R> {
       R valueOf(String name, int age);
    }

You can construct instances of User type like this

    User user1 = User.valueOf("John", 19);

User-class will have useful toString, equals and hashCode methods.

    System.out.println(user1.toString());

The following string will be printed:

User.ValueOf{name=John, age=19}

Equals and hashCode methods use field values instead of object-identity.

ADT4J generated classes are always immutable. There is no way to directly modify any fields of generated classes.

To access actual field values stored in an instance of User-type. You'll have to use visitor pattern.

    void printName(User user) {
        String name = user.access(new UserVisitor<String>() {
            @Override
            public String valueOf(String name1, int age) {
                return name1;
            }
        });
        System.out.println(name);
    }

This seems to be way too heavyweight. ADT4J provide a way to get rid of visitor pattern for simple field access. ADT4J can automatically generate getters. We need to modify UserVisitor interface to obtain getters from adt4j.

    @GenerateValueClassForVisitor(className = "User")
    @Visitor(resultVariableName="R")
    interface UserVisitor<R> {
       R valueOf(@Getter(name = "getName") String name, @Getter(name = "getAge") int age);
    }

Out field access example will become

    void printName(User user) {
        System.out.println(user.getName());
    }

Suppose that you have Point class. And you want to be able to move point horizontally. Like this:

   Point point1 = Point.valueOf(10, 3);
   Point point2 = moveRight(point1, 5);

moveRight function will look like this:

   Point moveRight(Point base, int deltaX) {
      return Point.valueOf(base.getX() + deltaX, base.getY());
   }

You can have family of such functions like:

   Point moveRight(Point base, int deltaX) {
      return Point.valueOf(base.getX() + deltaX, base.getY());
   }

   Point moveLeft(Point base, int deltaX) {
      return Point.valueOf(base.getX() - deltaX, base.getY());
   }

   Point moveToOrigin(Point base, int deltaX) {
      return Point.valueOf(0, base.getY());
   }

This seems quite right but it has a problem if you, for example, modify your point class to include color. You'll have to modify all this functions to thread color information through point instances:

   Point moveRight(Point base, int deltaX) {
      return Point.valueOf(base.getX() + deltaX, base.getY(), base.getColor());
   }

   Point moveLeft(Point base, int deltaX) {
      return Point.valueOf(base.getX() - deltaX, base.getY(), base.getColor());
   }

   Point moveToOrigin(Point base, int deltaX) {
      return Point.valueOf(0, base.getY(), base.getColor());
   }

You can refactor these methods to protect for future modifications of Point data-type:

   Point moveRight(Point base, int deltaX) {
      return withX(base, base.getX() + deltaX);
   }

   Point moveLeft(Point base, int deltaX) {
      return withX(base, base.getX() - deltaX);
   }

   Point moveToOrigin(Point base, int deltaX) {
      return withX(base, 0);
   }

If you ever change your Point data-type only withX method needs to be adjusted.

ADT4J allows you to automatically generate withX method for you. You can generate Point class like this:

    @GenerateValueClassForVisitor(className = "Point")
    @Visitor(resultVariableName="R")
    interface PointVisitor<R> {
       R valueOf(@Getter(name = "getX") @Updater(name = "withX") int x, @Getter(name = "getY") @Updater(name = "withY") int y);
    }

Methods for moving point become

   Point moveRight(Point base, int deltaX) {
      return base.withX(base.getX() + deltaX);
   }

   Point moveLeft(Point base, int deltaX) {
      return base.withX(base.getX() - deltaX);
   }

   Point moveToOrigin(Point base, int deltaX) {
      return base.withX(0);
   }

Disjoint-union types

With disjoint-union types you obtain full power of algebraic data types.

Lets examine the following example. Here we define possible permission required to obtain some resource.

    @GenerateValueClassForVisitor(className = "Permission")
    @Visitor(resultVariableName="R")
    interface PermissionVisitor<R> {
        R administrator();
        R groupMember(Group group);
        R onlyUser(User user);
    }

Permission class will be generated and we will be able to use visitor-pattern to test if some user have access to given resource.

    boolean isAccessGranted(final User user, Resource resource) {
        Permission required = resource.requiredPermission();
        return required.accept(new PermissionVisitor<Boolean> () {
            @Override
            public Boolean administrator() {
                return user.isAdministrator();
            }

            @Override
            public Boolean groupMember(Group group) {
                return group.contains(user);
            }

            @Override
            public Boolean onlyUser(User requiredUser) {
                return requiredUser.equals(user);
            }
        });
    }

If you miss some cases this will be compile-time error and this kind of bug will never slip into production.

All visitor-interface methods become constructor-methods in generated class. We can use any one of these method to construct class instance:

Permission permission1 = Permission.onlyUser(user1);

permission1 instance represents onlyUser-case. onlyUser-method will always be called when permission1 accepts visitor.

Resource resource = new Resource(permission1);
isAccessGranted(user1, resource); // Should always be true

Null-values

You should always add @Nullable annotation to make any field nullable. Otherwise null checks are generated and exception is thrown upon construction:

        @GenerateValueClassForVisitor
        @Visitor(resultVariableName="R")
        interface Record<T, R> {
            R valueOf(T mandatory1, Object mandatory2, @Nullable Object optional);
        }

Extending generated classes

We will use an implemetation of optional-type similar to Optional class provided by Java 8 .

    @GenerateValueClassForVisitor(className = "OptionalBase")
    @Visitor(resultVariableName="R")
    interface OptionalVisitor<T, R> {
        R present(@Nonnull T value);
        R missing();
    }

In the example above OptionalBase class will be generated.

You can extend generated class to add more methods like this:

    public class MyOptional<T> extends OptionalBase<T> {
        public static <T> MyOptional<T> missing() {
            return new MyOptional<>(OptionalBase.missing());
        }

        public static <T> MyOptional<T> present(T value) {
            return new MyOptional<>(OptionalBase.present(value));
        }

        private MyOptional(OptionalBase<T> value) {
            // protected constructor from OptionalBase class
            super(value);
        }

        //
        // equals and hashCode are correctly inherited from OptionalBase
        //

        public <U> MyOptional<U> flatMap(final Function<T, MyOptional<U>> function) {
            return accept(new OptionalVisitor<T, MyOptional<U>>() {
                @Override
                public MyOptional<U> missing() {
                    return MyOptional.missing();
                }

                @Override
                public MyOptional<U> present(T value) {
                    return function.apply(value);
                }
            });
        }
    }

Now you have MyOptional class similar to Optional class provided by Java 8.

You can use it to chain optional operations:

With Java 8 syntax:

    lookup(key1).flatMap((key2) -> lookup(key2));

or with anonymous classes:

    lookup(key1).flatMap(new Function<String, MyOptional<String>>() {
        public MyOptional<String> apply(String key2) {
            return lookup(key2);
        }
    });

Visitor-interface can be defined as inner-interface enclosed into your extended class:

    public class MyOptional<T> extends OptionalBase<T> {
        public static <T> MyOptional<T> missing() {
            return new MyOptional<>(OptionalBase.missing());
        }

        public static <T> MyOptional<T> present(T value) {
            return new MyOptional<>(OptionalBase.present(value));
        }

        private MyOptional(OptionalBase<T> value) {
            // protected constructor from OptionalBase class
            super(value);
        }

        // ...

        @GenerateValueClassForVisitor(className = "OptionalBase")
        @Visitor(resultVariableName="R")
        interface OptionalVisitor<T, R> {
            R present(@Nonnull T value);
            R missing();
        }
    }

Recursive data types and open-recursion

One of the most common examples of algebraic data types is list type.

To generate List data type we may try to use something like this:

    @GenerateValueClassForVisitor
    @Visitor(resultVariableName = "R")
    interface ListVisitor<T, R> {
        R empty();
        R prepend(T head, List<T> tail);
    }

But there is no List class yet. We can't reference it since it is to be generated. Above declaration creates unbreakable cycle and will result in compile-time error.

But still we can create List class with a trick known as open-recursion.

    @GenerateValueClassForVisitor
    @Visitor(resultVariableName = "R")
    interface OpenListVisitor<L, T, R> {
        R empty();
        R prepend(T head, L tail);
    }

We make list-type a type-variable here. And hypothetically it can be anything. We can constrain this type-variable by extending generated class.

    class List<T> extends OpenList<List<T>, T> {
        public interface Visitor<T, R> extends OpenListVisitor<List<T>, T, R> {
        }

        public <R> R accept(Visitor<T, R> visitor) {
            return super.accept(visitor);
        }

        public int length() {
            return this.accept(new Visitor<T, Integer> () {
                @Override
                public Integer empty() {
                    return 0;
                }

                @Override
                public Integer prepend(T head, List<T> tail) {
                    return 1 + tail.length();
                }
            });
        }
    }

List class is an extention of OpenList class with the constraint that tail is always of type List. List.Visitor interface is narrowed version of OpenListVisitor interface crafted specifically towards instances of List class.

ADT4J allows you to avoid manual extention of generated classes when recursive data-types are required. To achieve this goal selfReferenceVariableName parameter is used.

    @GenerateValueClassForVisitor(className = "List")
    @Visitor(resultVariableName = "R", selfReferenceVariableName = "L")
    interface OpenListVisitor<L, T, R> {
        R empty();
        R prepend(T head, L tail);
    }

Above code will generate List class that will accept instances of OpenListVisitor-interface as a visitor in only case when type-variable L is set to List-class itself.

    // Type of second argument of prepend method is List:
    List<Integer> list1 = List.prepend(1, List.prepend(2, List.<Integer>empty()));

    // Any other type is type-error:
    // List<Integer> list2 = List.prepend(1, "tail");

See adt4j-examples project for more complete examples.