Powerful, extensible code with Tagless Final in … Java!

John McClean
8 min readDec 4, 2018

--

Tagless final is all the rage in the Scala community at the moment. It is a technique that allows you to define the general structure of the code to be executed, such that we can configure different implementations (or interpreters) to inject effects such as Optionality, Asynchronicity, Parallelism, Non-Determinism, Error Handling depending on our needs. Need to simplify a highly concurrent piece of code for a test case so that you can debug a nasty production issue? With Tagless final — no problem!

Defining Mini-DSLs

Much like the Free Monad, Tagless Final allows us to define mini-DSLs and apply different interpreters to them. Unlike the Free Monad, Tagless Final is (relatively) simple.

A high level overview of the process

We need to define the operations available in our mini-language (in Tagless Final parlance — the Algebra, in the Java-world an Interface with some default methods) — methods such as createOrder, deleteOrder, listOrders go here.

We define our programs abstractly in terms of the Algebra. E.g. createOrder -> saveOrder -> listOrders

We provide implementations of our i̶n̶t̶e̶r̶f̶a̶c̶e̶ (sorry I mean Algebra).

So far, so Object Oriented.

A higher level of abstraction (or generality)

The key difference from standard Java is that the i̶n̶t̶e̶r̶f̶a̶c̶e (Algebra) is defined at a higher level of abstraction. Using the library KindedJ we can define a higher order generic type something like this

Hk<W,Order> createOrder(User user, OrderData data);

Hk is an interface that higher order types (such as the Monadic types in Cyclops) can implement, and W is the ‘witness’ type that let’s know which higher order type we are working with. More concretely a Cyclops Option would look something like this

Hk<option,Order> option = Option.some(order);

The Monad ‘pattern’

The nice thing about Monads is they all implement the same general suite of methods (of / map / flatMap and so on) that all behave in predictable ways. This means that if we define our programs to work with Monads, and define our Algebra in a generic manner so that the concrete Monadic type is pluggable — we can reuse the same Algebra and ‘program’ definition with many different Monadic datastructures. In fact we aren’t even constrained to using Monads (but that is for another article!)

Monads all do something different

Option : represents Optionality

Future : represents an asynchronously calculated value

Vector : represents something that can be absent, have one value or many values

Try : represents something that can fail

And so on. By defining our interfaces in a very general way, we can easily extend our application by pluggin in the behaviour we need by switching Monad types. As we shall see later, we can also mix monad types and stack them up too!

Tagless final v Free

A previous blog series dived into the details of creating mini-DSLs using the Free Monad in Java — it may be instructive to compare and contrast the approaches as Tagless Final appears a lot more straightforward.

DSLs with the Free Monad in Java 8 : Part I

Simulating Higher Kinded Types in Java

DSLs with the Free Monad in Java 8 : Part II

Let’s move on to an example

To start with we will convert the simple Account credit and debit example from Chapter 1 of Debasish (দেবাশিস্) Ghosh’s awesome Functional and Reactive Domain Modeling

The first step is to define our algebra or mini language, we could sketch this out as something like the ability to debit or credit an bank account.

AccountAlgebra{

debit(Account account, double amount);
credit(Account account, double amount);

}

We might define an interface for this in traditional Java something like this

interface AccountAlgebra{

Account debit(Account account, double amount) throws Exception;
Account credit(Account account, double amount) throws Exception;

}

But if you have been following the Dysfunctional Programming in Java series then you’ll know that throwing Exceptions isn’t good functional practice. A better idea would be to return an Either that captures the error as it’s left type value, or even an Option that is empty if debiting or crediting failed.

E.g. using Either

interface AccountAlgebra{

Either<AccError,Account> debit(Account account, double amount);
Either<AccError,Account> credit(Account account, double amount);

}

Using Option

interface AccountAlgebra{

Option<Account> debit(Account account, double amount);
Option<Account> credit(Account account, double amount);

}

We live in a reactive world and perhaps we should consider debit & credit should be executed asynchronously, we could return a Future here instead.

interface AccountAlgebra{

Future<Account> debit(Account account, double amount);
Future<Account> credit(Account account, double amount);

}

One of the nice things about tagless final is.. we don’t have to choose!

A higher level of Abstraction

We simply define AccountAlgebra at a higher level of abstraction with KindedJ :-

interface AccountAlgebra<W>{

Hk<W,Account> debit(Account account, double amount);
Hk<W,Account> credit(Account account, double amount);

}

I’m using Cyclops, which defines an interface Higher that extends Hk from KindedJ and adds some handy conversion methods.

interface AccountAlgebra<W>{

Higher<W,Account> debit(Account account, double amount);
Higher<W,Account> credit(Account account, double amount);

}

The type parameter W in the above example represents the monad type which we can plug in at compile time. If we want to use the IO monad to perform the actions asynchronously we define an implementation (or interpreter) like so :-

class AccountIO implements AccountAlgebra<io> {

Higher<io, Account> debit(Account account, double amount) {
return IO.of(()->account,exec)
.map(a->a.debit(amount));
}

Higher<io, Account> credit(Account account, double amount) {
return IO.of(()->account,exec)
.map(a->a.credit(amount));
}
}

Constructing a mini-program

With the operators available to us we could construct a simple program to transfer money from one account to another.

class TransferManager {     Account from;  
Account to;
}

We should also pass in our Algebra (or interface that defines the allowable operations)

class TransferManager<W>{     Account from;  
Account to;
AccountAlgebra<W> acountService;
}

We’re going to be working with Monads. It’s not normally possible to define an generic Interface for working with Monads in Java (as we can’t directly express higher kinded types — or generic type parameters that themselves accept a type parameter), but we can do it indirectly thanks to KindedJ. Cyclops defines such a higher kinded Monad interface for us.

class TransferManager<W>{     Account from;  
Account to;
AccountAlgebra<W> acountService;
Monad<W> monad;
}

Now we can leverage our AccountAlgebra with any Monad type (we only need to implement the Monad interface!).

Defining our transfer method

We can define a method to transfer money from each of these accounts

public ??? transfer(double amount)

Inside transfer we can compose the building blocks of our mini-language (defined in AccountAlgebra) to transfer between accounts.

given an amount

debit account ‘from’

credit account ‘to’

return both accounts.

Because we are working with monads, we could call the of (or unit) method on our Monad Interface to create a instance which contains amount, then we can compose calls to AccountAlgebra::credit and AccountAlgebra::debit via flatMap.

monad.flatMap(monad.of(amount), amount-> {  return monad.flatMap(accountService.credit(to,amount),account-> {                return accountService.debit(from,amount);      });
});

Pretty ugly!

But this is a standard pattern. We can easily put together a generic class that will orchestrate these calls for us and allow us to clean up our code (in Cyclops this is the Do builder)

Do.forEach(monad)
._of(amount)
.__(this::debitFrom)
.__(_1(this::creditTo))
.yield(__23(Tuple::tuple))

To get a nice fluent flow with method references all we need to do is define a couple of helper methods

private Higher<W,Account> debitFrom(double amount){
return accountService.debit(from,amount);
}
private Higher<W,Account> creditTo(double amount){
return accountService.credit(to,amount);
}

When we construct our program object all we need to do is pass in an instance of our AccountIO implementation of AccountAlgebra and the IO Monad Instance. If we want to change to a blocking implementation we could plugin an Option based implementation of the Algebra and it’s Monad instance (for example).

Combining Monads

It’s pretty straight forward to combine different monad types also.

Most Tagless final blogs seem to add some logging at this point (and I don’t really see any reason to break with tradition). So let’s define the algebra for our logging DSL.

LogAlgebra {

void info(String message);
void error(String message);

}

Which can translate into Tagless Final form

LogAlgebra<W> {

Higher<W,Void> info(String message);
Higher<W, Void> error(String message);

}

And given a LogAlgebra instance, and an AccountAlgebra instance

LogAlgebra<W> logService;
AccountAlgebra<W> accountService;

We can define a method to log account some account details

Higher<W, Void> logBalance(Account a) {
return logService.info("Account balance " + a.getBalance());
}

And we can then plug this into our for comprehension

Do.forEach(monad)
._of(amount)
.__(this::debitFrom)
.__(_2(this::logBalance))
.__(__1(this::creditTo))
.__(___4(this::logBalance))
.yield(____24(Tuple::tuple))

Because we have the same type parameter (which determines the monad type) in both the Log and Account Algebras, everything composes together quite nicely.

LogAlgebra<W> logService;
AccountAlgebra<W> accountService;

But what if they were different?

If the Monad types (as determined by the type parameters specified) were W1 and W2 rather than just W for both :-

LogAlgebra<W1> logService;
AccountAlgebra<W2> accountService;

This is still no problem! We just need to make sure we do a conversion back to one of the types (in this case W2). A NaturalTransformation is an interface for converting between two different monadic structures holding the same type :-

@FunctionalInterface
interface NaturalTransformation<W1,W2>{
<T> Higher<W2, T> apply(Higher<W1, T> a) ;
}

We pass in Monad <W1,T> and get back Monad<W2,T>.

A logging algebra

Let’s define a blocking LogAlgebra iterpreter that uses the Identity Monad (rather than IO or another non-blocking alternative)

LogID implements LogAlgebra<identity>{ 

Higher<identity, Void> info(String message) {
return Identity.of(message).peek(logger::info);
}

@Override
Higher<identity, Void> error(String message) {
return Identity.of(message).peek(logger::error);
}

W1 is the Monad type we want to convert from, and W2 is the Monad type we want to convert to. E.g. to convert between an Identity and an IO the NaturalTransformation would look like this

class IdentityToIO implements NaturalTransformation<identity, io> {

public <T> IO<T> apply(Higher<identity, T> a) {
return a.convert(Identity::narrowK)
.to(IO::sync);
}
}

Using NaturalTransformations to convert between higher kinded Monads is straight forward as well

NaturalTransformation<W2,W1> natTrans;Higher<W1, Void> logBalance(Account a) {
return logService.info("Account balance " + a.getBalance())
.convert(natTrans.asFunction());
}

Which leaves our program — pretty much identical to how it was before

Do.forEach(monad)
._of(amount)
.__(this::debitFrom)
.__(_2(this::logBalance))
.__(__1(this::creditTo))
.__(___4(this::logBalance))
.yield(____24(Tuple::tuple))

Stacking Monads

It’s possible to stack algebras and interpreters, plugging in different Monads to different effect at each layer. Let’s add in a data store and use to store account balances. In plain Java the Store Algebra might look something like this :-

StoreAlgebra<K,V> {
Option<V> get(K key);
void put(K key, V value);
}

With the Tagless Final encoding it becomes :-

StoreAlgebra<W,K,V> {
Higher<W, Option<V>> get(K key);
Higher<W, Void> put(K key, V value);
}

We can modify our AccountAlgebra to allow us to make use of the Store, first by making is available somehow on the algebra (the hacky but easy thing to do is just to add a couple of methods directly to the interface definition).

AccountAlgebra<W> {

StoreAlgebra<W,Long,Account> store();
Do<W> forEach();

Higher<W,Option<Account>> debit(Account account, double amount)
Higher<W, Option<Account>> credit(Account account, double amount);
}

We can even plugin a default implementation that makes use of Store Algebra

AccountAlgebra<W> {

StoreAlgebra<W,Long,Account> store();
Do<W> forEach();
Higher<W,Option<Account>> debit(long id, double amount){
return forEach().__(store().get(id))
.__(acc -> acc.debit(amount))
.__(_1(a -> store().put(a.getId(), a)))
.yield_1((a, b) -> a);

}

Higher<W, Option<Account>> credit(long id, double amount){
return forEach().__(store().get(id))
.__(acc -> acc.credit(amount))
._1(acc -> store().put(acc.getId(), acc))
.yield_1((a, b) -> a);
}

It’s a bit messier this time as we haven’t defined helper methods to use as method references.

Our program code remains very simple (with the logging removed)

Do.forEach(monad)
._of(amount)
.__(this::debitFrom)
.__(_1(this::creditTo))
.yield(__23(Tuple::tuple))

Cyclops : a higher kinded functional library for Java

If you want to try this out at home, import the cyclops library for functional programming in Java and play about with the examples, all the code is on the github repo.

Maven

<dependency>
<groupId>com.oath.cyclops</groupId>
<artifactId>cyclops-pure</artifactId>
<version>10.1.0</version>
</dependency>

Gradle

compile group: 'com.oath.cyclops', name: 'cyclops', version: '10.1.0'

--

--

John McClean

Architecture @ Verizon Media. Maintainer of Cyclops. Twitter @johnmcclean_ie