Powerful, extensible code with Tagless Final in … Java!
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'