Applicative is just another concept similar in meaning and history to Functors and Monads. Besides explaining the theory behind it, I will implement a simple Applicative. I will also use Optional, to show what advantages Applicatives may give us.
The source code for this article is available in GitHub repository.
Why Should We Care About Applicatives?
First of all, Applicatives are the intermediate construct between Functors and Monads. They are more powerful than Functors but less powerful than Monads. Applicatives are also great for performing various context-free computations like parsers or traversables instances.
Thanks to the fact that all Applicatives are Functors, Applicatives can make the whole journey from Functors to Monads easier, playing the role of a bridge between both concepts.
What Is An Applicative?
An Applicative or Applicative Functor, in longer form, is mostly a functional programming concept as they do not have a direct counterpart in category theory.
Their indirect counterpart in math is known as lax monoidal functors. It was introduced in 2008 by Conor McBride and Ross Paterson in their paper “Applicative programming with effects”. Additionally, as all Applicatives are Functors, all Laws required by Functors have to be also met by Applicatives instances.
What is more, Applicatives do not allow connecting outputs and inputs of operations on wrapped values into sequences. That is why their main focus in the world of software is to perform context-free operations on values inside an arbitrary wrapper.
Unfortunately, it is not a trivial thing to implement in Java, so we basically end up with such an ability anyway.
Applicative Laws
As with both previously described data types, Applicatives also have laws to fulfill. In fact, Applicatives have the highest number of such laws, namely: Identity, Homomorphism, Interchange, Composition.
Classically, a few assumptions before we start:
- f is a function mapping from type A to type B
- g is a function mapping from type B to type C
1. Identity
Applying the identity function to a value wrapped with pure should always return an unchanged value.
Applicative<Integer> identity = Applicative.pure(x).apply(Applicative.pure(Function.identity()));
boolean identityLawFulfilled = identity.valueEquals(Applicative.pure(x));2. Homomorphism
Applying a wrapped function to a wrapped value should give the same result as applying the function to the value and then wrapping the result with the usage of pure.
Applicative<String> leftSide = Applicative.pure(x).apply(Applicative.pure(f));
Applicative<String> rightSide = Applicative.pure(f.apply(x));
boolean homomorphismLawFulfilled = leftSide.valueEquals(rightSide);3. Interchange
Applying the wrapped function f to a wrapped value should be the same as applying the wrapped function which supplies the value as an argument to another function, to the wrapped function f.
// As far as I can tell it is as close, to original meaning of this Law, as possible in Java
Applicative<String> interchangeLeftSide = Applicative.pure(x).apply(Applicative.pure(f));
Supplier<Integer> supplier = () -> x;
Function<Supplier<Integer>, String> tmp = i -> f.apply(i.get());
Applicative<String> interchangeRightSide = Applicative.pure(supplier).apply(Applicative.pure(tmp));
boolean interchangeLawFulfilled = interchangeLeftSide.valueEquals(interchangeRightSide);4. Composition
Applying the wrapped function f and then the wrapped function g should give the same results as applying wrapped functions composition of f and g together.
// As far as I can tell it should be in line with what is expected from this Law
Applicative<Long> compositionLeftSide = Applicative.pure(x).apply(Applicative.pure(f)).apply(Applicative.pure(g));
Applicative<Long> compositionRightSide = Applicative.pure(x).apply(Applicative.pure(f.andThen(g)));
boolean compositionLawFulfilled = compositionLeftSide.valueEquals(compositionRightSide);Additionally, because the Applicative is an extension of a Functor, your instance should fulfill both laws from the Functor: Identity and Composition.
Fortunately, both laws imposed by the definition of Functor are already among four laws of Applicative, so they should be fulfilled by the Applicative definition itself.
Creating an Applicative
The first thing you will need is a parameterized type M<T>. Moreover, you will need two methods:
1. apply (or ap) responsible for performing operations. Here you pass a function already wrapped in our context and that operates on the value in our context. This method should have the following signature M<T> (M<T->R>).
2. pure which is used to wrap your value and has the following signature M<T>(T).
What is more, because all Applicatives are Functors, you get a map method with signature M<T> (T -> R) by definition.
Be Aware:
There is a second, probably more common, equivalent for Applicative in such a case. Instead of the apply method, we have a product method with signature M<(T, R)>(M<T>, M<R>). Such Applicatives must obey different Laws, namely: Associativity, Left Identity, and Right Identity. Their descriptions and examples are included in the GitHub repository.
Implementing an Applicative
package org.pasksoftware.applicative.example;
import org.pasksoftware.functor.Functor;
import java.util.Optional;
import java.util.function.Function;
public final class OptionalApplicative<A> implements Functor<A> {
private final Optional<A> value;
private OptionalApplicative(A value) {
this.value = Optional.of(value);
}
private OptionalApplicative() {
this.value = Optional.empty();
}
static <A> OptionalApplicative<A> pure(A value) {
return new OptionalApplicative<>(value);
}
<B> OptionalApplicative<B> apply(OptionalApplicative<Function<A, B>> f) {
Optional<B> apply = f.value.flatMap(value::map);
return apply.map(OptionalApplicative::new).orElseGet(OptionalApplicative::new);
}
@Override
public <B> Functor<B> map(Function<A, B> f) {
return apply(pure(f));
}
// For sake of asserting in Example
public boolean valueEquals(Optional<A> s) {
return value.equals(s);
}
}Above, you can see ready Applicative implementation, but why does it look the way it looks?
The base of this implementation is the parameterized class with the immutable field named “value”, which is responsible for storing the value. Then, you can see private constructors that make it impossible to create an object in any other way than through a wrapping method – pure.
Next come two methods unique for Applicatives – pure used for wrapping values in Applicative context and apply for using wrapped functions to wrapped values. Both are implemented using Optional and its methods, so they are fully null safe.
Then you have a method map coming from the Functor, it is an “additional” method that may become useful if you are interested in performing some operation over the value inside the Applicative.
Applicative Usage Example
Let’s move on to presenting a simple example of Applicative usage along with a short description why it may be better than a non-applicative approach.
package org.pasksoftware.applicative.example;
import java.util.Optional;
import java.util.function.Function;
public class Example {
public static void main(String[] args) {
int x = 2;
Function<Integer, String> f = Object::toString;
// Task: applying wrapped function to wrapped value
// Non-applicative
Optional<Function<Integer, String>> optionalFunction = Optional.of(f);
Optional optional = Optional.of(x);
// One-liner
// Optional.of(x).flatMap(v -> Optional.of(f).map(of -> of.apply(v)));
Optional result = optional.flatMap(v -> optionalFunction.map(of -> of.apply(v)));
// Applicative
OptionalApplicative applicative = OptionalApplicative.pure(x);
OptionalApplicative<Function<Integer, String>> pure = OptionalApplicative.pure(f);
// One-liner
// OptionalApplicative.pure(x).apply(OptionalApplicative.pure(f));
OptionalApplicative applicativeResult = applicative.apply(pure);
assert applicativeResult.valueEquals(result);
System.out.println("Values inside wrappers are equal");
}
}Above you can see for yourself the possible benefits given by Applicative abstraction over plain Optional-based approach. First of all, Applicative-based code is simpler and easier to understand than the Optional one, it does not require any complex things like embedded map calls.
In fact, the user does not even know that they are using Applicative with Optional features, so I was able to provide better encapsulation. In my opinion, the pros of using such abstraction outbalance the cost of writing one.
Closing Thoughts
Applicatives can be a great solution especially when you need to handle context-free computations. They are very useful when you have to write parsers or traversables.
Additionally, being the intermediate concept between Functors and Monads, they can make your journey through category theory data types easier.
Thank you for your time.