RxSealedUnions

by pakoito

pakoito /RxSealedUnions

Compile-time checked Unions of different types for Domain Modeling [STABLE]

130 Stars 4 Forks Last release: about 4 years ago (1.1.0) Other 30 Commits 3 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

RxSealedUnions

This repository is a RxJava backport of Java 8's JavaSealedUnions library.

For the RxJava 2.X version please go to RxSealedUnions2.

EXAMPLES

You can see RxSealedUnions used through the Functional Android Reference app released as a companion example for the talk on Fully Reactive Apps at Droidcon UK 2016.

DISTRIBUTION

Add as a dependency to your

build.gradle
    repositories {
        ...
        maven { url "https://jitpack.io" }
        ...
    }

dependencies {
    ...
    compile 'com.github.pakoito:RxSealedUnions:1.1.0'
    ...
}

or to your

pom.xml
    
        
            jitpack.io
            https://jitpack.io
        
    

<dependency>
    <groupid>com.github.pakoito</groupid>
    <artifactid>RxSealedUnions</artifactid>
    <version>1.1.0</version>
</dependency>

ACKNOWLEDGEMENTS

This library was heavily inspired by RxEither and the wonderful Domain Driven Design (DDD) talk by Scott Wlaschin. Another similar talk with the full Tennis kata we'll use as an example below is Types + Properties = Software by Mark Seemann.

RATIONALE

RxSealedUnions brings unions into idiomatic Java 6 using Reactive Extensions to allow for better domain modelling. It can also help representing sealed classes, but that is not the main focus. Chaining operations and monadic composition using RxSealedUnions is also outside the scope of the library, but any union can be lifted to Observables as shown by RxEither.

Java's type system is considered not very powerful although it contains most OOP niceties. Some of the most known absences are tagged unions and sealed classes. Sealed classes are available in languages like Kotlin, or C#. Tagged unions are common on Swift and Rust.

The most frequent approach to solve this modelling problem in Java is having a base class or interface

IMyContract
and implementing several of
IChildContractPlusExtras
for public scopes. Another alternative is having a public
abstract
class that is inherited by a small set of package-only classes. The problem with the first approach is the possibility of breaking encapsulation and being able to implement the interface by a 3rd party outside the desired outcomes. The second approach hides the implementations for you, which requires the use of runtime tools like
instanceof
to handle. Both cases have one common problem: they only allow association of classes that are of the same type, or in the same hierarchy.

The intent of this library is to create a set of classes that can host one element of one to several arbitrary complex types. Historically other libraries like Guava or RxJava have provided chainable types to solve this issue:

  • The base case is

    Result
    (also known as
    Optional
    ), which is the union of two types:
    Some
    and
    None
    .
  • The next case is

    Either
    or
    Try
    , which respectively wrap the union of two arbitrary types
    Left
    and
    Right
    , or
    Success
    and
    Failure
    .

For a higher number of parameters no abstraction is usually provided, and it's when other languages change to explicit union types. As Java does not have unions on the language, we have to continue abstracting away with 3 types (Left, Middle and Right), 4 types, 5 types...etc.

We're calling them

Union1
for
Result
/
Optional
,
Union2
for
Either
/
Try
,
Union3
...up to
UnionN
, which for this library would be
Union9
. A special case for
Union0
is given for cases where you simply require to box a type for design purposes, and future improvements.

I heavily recommend watching the DDD talk linked above first to see what solutions this library provides compared to chainable types. Unions are used for intra-layer modelling, chainables are more fit for inter-layer communication.

INTERFACES

Now that we understand what the abstraction has to provide, we have to define a public API:

What defines a union?

It's a type that allows storage of a single element that can be from any of 2-N different types. It's container and implementation agnostic. The only requirement is to have ways to retrieve the data inside. To be properly composable it requires using interface composition rather than abstract inheritance.

What belongs in the interface?

It needs to be able to dereference the types to obtain a single, unequivocal, result. It should avoid extra operations, not throw exceptions, and not be able to represent error states.

How is this done in other languages or frameworks?

  • Nested ifs:
if (union.isOne()) {
    One element = union.getOne();
    /* do something with one */ 
} else if (union.isTwo()) {
    Two element = union.getTwo();
    /* do something with two */ 
} else...

... and so and so until 9. Problem? The api can be dereferenced using

getXXX()
without calling any of the
isXXX()
methods, leading to exceptions and unexpected states. It adds one unneeded extra operation.
  • Subtyping:
MyElement element = createElement();
if (element instanceof One) {
    One one = (One)element;
    /* do something with one */
} else if (element instanceof Two) {
    Two two = (Two)element;
    /* do something with two */
} else...

It suffers from the same shortcomings as nested ifs: it requires programmer discipline to remember and check before casting, plus it leans to the same errors. The only change is that it now requires two operations:

instanceof
and a cast.
  • Pattern matching: not available in Java. But the intent of a pattern matcher is double: either continue to another piece of code, or return a single result element. This ties directly to the next two points.

  • Continuations: part of Inversion of Control, it's a principle where a function decides what to do with the execution flow after it's done. In this case you provide one method per type in the union indicating how the operation has to continue after dereferencing. It branches execution synchronously into the continuations without having to check types externally. It does not allow representing incorrect states, dereferencing unavailable types, or any other causes of Exceptions save for NPEs.

  • Joining: provide a function per element in the union that maps them back into a single common type that the current execution flow knows how to use. The mapping is applied synchronously and the flow continues on the same method. The caller is responsible of assuring the mapping operation is correct, or have a fallback case.

For the library I have chosen continuations and joining as the default methods in the interface.

NOTE: you should never ever require or implement

getXXX()
as a way of returning a type inside the union. It defeats the purpose of the library. Direct dereference is error-prone, tends to be abused by programmers, and has been cited as a mistake when creating
Optional
-like libraries.

Final implementation of Union2

public interface Union2 {

void continued(Action1<first> continuationFirst, Action1<second> continuationSecond);

<r> R join(Func1<first r> mapFirst, Func1<second r> mapSecond);

}

And one example usage:

Union2 information = serverRequester.loggedAccountInformation();

// Get a single piece of information from either List totalPurchases = information.join(user -> user.getPurchases(), team -> team.getAccountManager().getPurchases());

// Or you can finish the current method and continue somewhere else depending on the type information.continued(UserPageTemplater::start(), TeamPageTemplater::start());

Creation

Part of creating a union is that the union itself is a new type and has to be represented too. For this case it's been included one Factory interface per UnionN that can be extended and required to create each one of the elements in the union:

public interface Factory {

Union2<left right> first(Left first);

Union2<left right> second(Right second);

}

USAGE

Generic unions

This set of classes are provided by the library to wrap any class regardless of its type. They come in flavours from

Union0
to
Union9
.
GenericUnions
is a class with factories for all the union types. Factories can be provided by calling one of
nulletFactory()
,
singletFactory()
,
doubletFactory()
,
tripletFactory()
,
quartetFactory()
,
quintetFactory()
,
sextetFactory()
,
septetFactory()
,
octetFactory()
and
nonetFactory()
. ```java public class LoggedInAccount { public final String id;
public long timestamp;

public final Union4 account;

public LoggedInAccount(String id, long timestamp, Union4 account){ this.id = id; this.timestamp = timestamp; this.account = account; }

}

public LoggedInAccount login(String id){ Union4.Factory quartetFactory = GenericUnions.quartetFactory(); Union4 user = database.getAccount(id) ? quartetFactory.third(database.requestAdmin(id)) : quartetFactory.first(database.requestUser(id)); LoggedInAccount account = new LoggedInAcctount(id, System.currentTimeMillis(), user); return account; } ```

Typed wrappers

In case you want your unions to be driven by your domain you have to create your own classes implementing the base interfaces. There are several recommended approaches:

Holder class with generic union

A domain class giving a more explicit naming and access to its methods and content.

REMINDER: Implement

getXXX()
as a way of returning a type inside the union defeats the purpose of unions.

public class Salute {

private static final Either.Factory<dog neighbour> FACTORY = GenericUnions.eitherFactory();

public static Salute dog(String name, int paws) {
    return new Salute(FACTORY.first(new Dog(name, paws)));
}

public static Salute neighbour(String name, String favouriteFood, boolean isLiked) {
    return new Salute(FACTORY.second(new Neighbour(name, favouriteFood, isLiked)));
}

private final Union2<dog neighbour> either;

Salute(Union2<dog neighbour> either) {
    this.either = either;
}

public void openDoor(Action1<dog> continueDog, Action1<neighbour> continueNeighbour) {
    return either.continued(continueDog, continueNeighbour);
}

public String rememberSalute(Func1<dog string> mapDog, Func1<neighbour string> mapNeighbour) {
    return either.join(mapDog, mapNeighbour);
}

}

// Example String salute = getSalute().rememberSalute(dog -> "Who's a good dog?", neighbour-> neighbour.isLiked? "Good morning, " + neighbour.name + "!" : "grunt"); getSalute().openDoor(dogSaluter::salute(), neighbourSaluter::salute());

Subclassing

This ties up to the inheritance approach, except it's sealed and explicit. It can be done by both abstract classes or interfaces.

As a personal rule I would avoid any inherited methods, overloading, or overriding in any of the child classes. Watch the DDD talk in the Acknowledgements section to better understand the use of union types as plain data.

The example below breaks this rule by adding a new method

valid()
. ```java public abstract class PaymentType implements Union3 {

public abstract boolean valid();

public static PaymentType card(String cardNo, String ccv) { return new CardPayment(cardNo, ccv); }

public static PaymentType paypal(String paypalNo, String password) { return new PayPalPayment(paypalNo, password); }

public static PaymentType bankTransfer(String accNo) { return new BankTransferPayment(accNo); }

}

class CardPayment extends PaymentType {

private final String cardNo;

private final String ccv;

CardPayment(String cardNo, String ccv) { this.cardNo = cardNo; this.ccv = ccv; }

public boolean valid() { return /* some logic here */ }

public void continued(Action1 continuationLeft, Action1 continuationMiddle, Action1 continuationRight) { continuationLeft.call(value); }

public T join(Func1 mapLeft, Func1 mapMiddle, Func1 mapRight) { return mapLeft.call(value); }

}

class PayPalPayment extends PaymentType {

private final String user;

private final String pwd;

CardPayment(String user, String pwd) { this.user = user; this.pwd = pwd; }

public boolean valid() { return /* some logic here */ }

public void continued(Action1 continuationLeft, Action1 continuationMiddle, Action1 continuationRight) { continuationMiddle.call(value); }

public T join(Func1 mapLeft, Func1 mapMiddle, Func1 mapRight) { return mapMiddle.call(value); }

}

class BankTransferPayment extends PaymentType {

private final String accountNo;

CardPayment(String accountNo){ this.accountNo = accountNo; }

public boolean valid() { return /* some logic here */ }

public void continued(Action1 continuationLeft, Action1 continuationMiddle, Action1 continuationRight) { continuationRight.call(value); }

public T join(Func1 mapLeft, Func1 mapMiddle, Func1 mapRight) { return mapRight.call(value); }

}

// Example

PaymentType payment = getUserPayment(); if (payment.valid()) { payment.continued(paymentService::byCard(), paymentService::withPayPal(), paymentService::viaBankTransfer()) } ```

DDD

The last approach is the recommended to make the most out of the principles described across this document, using types rather than inheritance or fields.

A complete version of the Tennis kata can be found in TennisGame.java along with usage tests at TennisGameTest.java ```java public abstract class Score {

Union4 getScore();

}

public final class Points extends Pair { }

public abstract class PlayerPoints {

Union4 getPlayerPoints();

}

public abstract class Advantage extends Player { }

public final class Deuce { }

public abstract class Game extends Player { }

public abstract class Player {

Union2 getPlayer();

} ```

License

Copyright (c) pakoito 2016

The Apache Software License, Version 2.0

See LICENSE.md

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.