What Is Applicative? Basic Theory for Java Developers
Are you a Java developer who wants to know the theory behind Applicatives? Here you will find a step-by-step tutorial that will help you understand them.
Join the DZone community and get the full member experience.
Join For FreeApplicative is just another concept similar in meaning and history to Functors and Monads. I have covered these two in my previous articles, and I think it is finally time to start closing this little series about the most commonly known functional abstractions. Besides explaining some details and theory, I will implement a simple Applicative. I will also use Optional, hopefully, one last time, to show what advantages Applicatives 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 traversable instances.
Additionally, all Applicatives are Functors, which may make implementing Applicatives easier if you have some experience with Functors. Thanks to this relation, Applicatives can make the whole journey from Functors to Monads easier, playing the role of a bridge between both concepts.
Moreover, they are usually easier to write than Monads. In more functional languages like Scala or Haskell, Applicatives tend to have more instances than Monads.
What Is An Applicative?
An Applicative Functor, or Applicative, in short, is a mostly functional programming concept. It was introduced in 2008 by Conor McBride and Ross Paterson in their paper Applicative programming with effects. Their indirect counterpart in category theory is known as lax monoidal functors. Additionally, please keep in mind that all Applicatives are Functors. Such a relation will have significant implications when we start discussing Applicative Laws and methods — it mostly implies that all Laws required by Functors have to be also met by Applicatives.
In the world of software, the main focus of Applicatives is, similarly to Monads, to wrap values in a particular context and then perform operations - to be exact, operations wrapped in the same context as the value. Contrary to Monads, Applicatives do not allow chain operations in the same ways as Monads do, with the output of one operation being the input of the other. Unfortunately, it is not so easily implementable in Java, so we basically end up with such an ability anyway. Furthermore, unlike Functors, Applicatives allow us to sequence our computations.
Applicative Laws
As 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, and Composition. In my opinion, Applicative Laws are also the hardest to understand at first sight, especially Homomorphism and Interchange, among all the three data types I described.
Classically, a few assumptions before we start:
- f is a function mapping from type T to type R
- g is a function mapping from type R to type U
Identity
Applying the identity function to a value wrapped with pure should always return an unchanged value.JavaReferentialApplicative<Integer> identity = ReferentialApplicative.pure(x).apply(ReferentialApplicative.pure(Function.identity())); identity.valueEquals(ReferentialApplicative.pure(x));
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.JavaReferentialApplicative<String> leftSide = ReferentialApplicative.pure(x).apply(ReferentialApplicative.pure(f)); ReferentialApplicative<String> rightSide = ReferentialApplicative.pure(f.apply(x)); leftSide.valueEquals(rightSide);
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 fJava// As far as I can tell it is as close, to original meaning of this Law, as possible in Java ReferentialApplicative<String> interchangeLeftSide = ReferentialApplicative.pure(x).apply(ReferentialApplicative.pure(f)); Supplier<Integer> supplier = () -> x; Function<Supplier<Integer>, String> tmp = i -> f.apply(i.get()); ReferentialApplicative<String> interchangeRightSide = ReferentialApplicative.pure(supplier).apply(ReferentialApplicative.pure(tmp)); interchangeLeftSide.valueEquals(interchangeRightSide);
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 togetherJava// As far as I can tell it should be in line with what is expected from this Law ReferentialApplicative<Long> compositionLeftSide = ReferentialApplicative.pure(x).apply(ReferentialApplicative.pure(f)).apply(ReferentialApplicative.pure(g)); ReferentialApplicative<Long> compositionRightSide = ReferentialApplicative.pure(x).apply(ReferentialApplicative.pure(f.andThen(g))); 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 the four laws of Applicative, so they should be fulfilled by the Applicative definition itself.
Now that you know what laws you have to fulfill, I can start talking about what exactly you need to implement your Applicative.
Creating an Applicative
What We Need To Implement The Applicative
The first thing you will need is parameterized type A<T>. The parameterized type is the cornerstone of all data types similar in structure to Applicatives, Monads, and Functors. Moreover, you will need two methods:
- 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<U> (M<T -> U>).
- pure which is used to wrap your value and has the following signature M<T>(T).
Additionally, because all Applicatives are Functors, you get a map method with signature M<R> (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 the signature M<(T, U)>(M<T>, M<U>). Such Applicatives must obey different Laws, namely: Associativity, Left Identity, and Right Identity. Their descriptions and examples are included in the GitHub repository.
Knowing what one needs to make an Applicative, I can start implementing one.
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?
Describing Applicative Implementation
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 of 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<Integer> optional = Optional.of(x);
// One-liner
// Optional.of(x).flatMap(v -> Optional.of(f).map(of -> of.apply(v)));
Optional<String> result = optional.flatMap(v -> optionalFunction.map(of -> of.apply(v)));
// Applicative
OptionalApplicative<Integer> applicative = OptionalApplicative.pure(x);
OptionalApplicative<Function<Integer, String>> pure = OptionalApplicative.pure(f);
// One-liner
// OptionalApplicative.pure(x).apply(OptionalApplicative.pure(f));
OptionalApplicative<String> 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.
Summing up
Despite being a concept of less power than Monads, 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.
Category Theory Data Types in Java
If this piece of text was interesting, you may also be interested in other articles from my series about category theory data types in Java:
Applicatives FAQ
What Is an Applicative?
An Applicative Functor, or Applicative, is a functional programming concept, all Applicatives are instances of Functors.
What Are Applicative Laws?
Every Applicative instance has to satisfy four laws: Identity, Homomorphism, Interchange, and Composition
What Do I Need to Implement an Applicative?
To implement an Applicative, you need a parameterized type M<T> and two methods: pure and apply, the method ‘map’ is inherited from the Functor, so we get it by definition.
Opinions expressed by DZone contributors are their own.
Comments