Railway Oriented Programming : A powerful Functional Programming pattern
There are three pictures of roads with different characteristics
- light traffic
- heavy traffic and
- heavy traffic without orderliness.
If our goal is to drive at the highest speed( without compromising on safety) Then 1 would be best option, between options 2 and 3, it would be 2 which is a better option. Why is that so? It is because the system is predictable, minimising surprises for the driver. Predictability derives from fact, that there is a well designed model(of dos and don’ts, cans and can’ts), which is well understood and followed by the drivers.
The correlation with a good model and speed is true for software development too, when the code “density” is low, it is possible to go fast, when the “density” is high, higher velocity can be achieved only with conformance to a well designed system of dos and don’ts.
Railway oriented programming is a model from the functional programming world, which has its dos and don’ts, cans and can’ts which solve categories of concerns elegantly. [Note:This name was coined by Scott Wlaschin, and some of the pictures in this post are taken from his slides]
Let’s learn about various parts of this model. The first is a function. A function is “block” which transforms an input to output. It has no state.
An example is provided below of function, which takes a String as input and returns its length
public class Identity<E> {
private final E data;
public Identity(E e) {
data = e;
}
public<V> Identity<V> map(Function<E, V> mapper){
return new Identity<V>(mapper.apply(data));
}
public E get(){
return data;
}
}
The code snippet is of the Identity functor. It accepts a function and returns a new instance of Identity. If an identity had state “Hello” and it maps over “String::length” it creates an identity, with value 5, inside it.
It can be visualised as below, there are two rails, with values “Hello” and “5”, connected with the String::length function. The rails form a track, connected by a block.
Now, let’s consider the following code snippet
Optional.ofNullable(s)
.map(String::length);
if s is of type String, null is a legal value for s. The above snippet results in a state, which can be visualised as belo
There is a track, which corresponds to non-empty, with a rails corresponding to “Hello” and 5 as example, and another track with rails corresponding to empty and empty. The Optional class is ensuring that length function is not applied to data in empty track.
public String f(String s) {
return (s.charAt(0) == 'a') ? null : s;
}
let’s consider a function f, which might return null for certain conditions
Optional.ofNullable(s).map(f).map(g);
when executing code like this, Optional, monitor the output of f, and changes track if required, this ensures that function g is not invoked on the empty track. The invariants of not invoking a function on data on the empty track, and moving to empty track when output of the f is null is maintained by the Optional type.
The model/visualisation can be extended to chaining with many functions
Another type, which can be reasoned with the Railway Track model is of Either. It is a multi-valued type, which is one of Integer or String in the example shown below.
Either<String, Integer> value = compute().map(i -> i * 2);
Either maintains two tracks one for left and another for right, its map operates on the right, and result stays on the right track.
Track aware functions
unlike, the lambda in the previous example which was only “aware” of the integer world, the lambda is the example below, is aware of two tracks, and can produce track-sensitive tracks.
Either<String, Integer> value = compute().flatmap(i -> Either.left("Error"));
The either type, like the Optional has the responsibility of invoking the functions(track insensitive and sensitive) on the right track.
Different types have different semantics for the tracks, in case of Optional, one of the track is the empty track and the other non-empty track. Optional provides the invariant of never invoking any function on the empty track, preventing null pointer exception. Similarly Either has a left track and a right track, with the left track usually representing error path, and the right track the happy path, which allows modelling and addressing of errors as a first class concern.
I have discovered multiple applications of the railway oriented pattern other than error handling. One such example is control flow. if-else as well as switches are statements in java. This can result in code with one of multiple return statements or multiple assignments to the same variable.
An alternative was built using the dual track model of Railway Oriented Programming. Its specification is provided below.
public final class Conditional<T> {public static <U> Conditional<U> of(Boolean condition, Supplier<U> supplier);public Conditional<T> or(Boolean condition, Supplier<T> supplier);public T orElse(Supplier<T> supplier) ;
}
Conditonal, is the expression equivalent of Switches. It has one track for false(unset) condition, and another for true(set) condition. The conditional type checks for conditions, if and only if it is in the unset condition. when the conditional is set, it stores the value from the associated supplier. Subsequent ors are ignored.
In the code snippet shown below, two ors and the orElse are no-ops.
public void testWithSuppliersAndCondition() {
Integer result = Conditional
.of(true, () -> 1))
.or(true, () -> 2)
.orElse(() -> 3);
assertEquals(result, ONE);
}
Summary: Types are a proven mechanism of enforcing invariants. By following dual/multiple track model of Railway Oriented Programming. invariants which prevent Null Pointer Exceptions, simplify code or ease Error Handling can be implemented.