DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • What Is a Functor? Basic Theory for Java Developers
  • What Is a Monad? Basic Theory for a Java Developer
  • Redefining Java Object Equality
  • Java vs. Scala: Comparative Analysis for Backend Development in Fintech

Trending

  • Java’s Next Act: Native Speed for a Cloud-Native World
  • A Guide to Container Runtimes
  • Breaking Bottlenecks: Applying the Theory of Constraints to Software Development
  • Unlocking AI Coding Assistants Part 3: Generating Diagrams, Open API Specs, And Test Data
  1. DZone
  2. Coding
  3. Java
  4. What Is Applicative? Basic Theory for Java Developers

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.

By 
Bartłomiej Żyliński user avatar
Bartłomiej Żyliński
DZone Core CORE ·
Jan. 20, 22 · Analysis
Likes (13)
Comment
Save
Tweet
Share
11.3K Views

Join the DZone community and get the full member experience.

Join For Free

Applicative 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
  1. Identity
    Applying the identity function to a value wrapped with pure should always return an unchanged value.

    Java
     
    ReferentialApplicative<Integer> identity = ReferentialApplicative.pure(x).apply(ReferentialApplicative.pure(Function.identity()));
    identity.valueEquals(ReferentialApplicative.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.

    Java
     
    ReferentialApplicative<String> leftSide = ReferentialApplicative.pure(x).apply(ReferentialApplicative.pure(f));
    ReferentialApplicative<String> rightSide = ReferentialApplicative.pure(f.apply(x));
    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

    Java
     
    // 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);
  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

    Java
     
    // 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

Java
 
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.

Java
 
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:

  1. Functors
  2. Monads

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.

Java (programming language) Functor Monad (functional programming) Functional programming

Opinions expressed by DZone contributors are their own.

Related

  • What Is a Functor? Basic Theory for Java Developers
  • What Is a Monad? Basic Theory for a Java Developer
  • Redefining Java Object Equality
  • Java vs. Scala: Comparative Analysis for Backend Development in Fintech

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!