What Is Applicative? Basic Theory for Java Developers

My application is just another concept similar in meaning and history to commentators and monads. I’ve covered these two in my previous articles, and I think it’s finally time to start closing this little series on the most common functional abstractions. Besides explaining some details and theory, I will make a simple practical application. I will also use the optional option, hopefully for the last time, to show the advantages that our applicants offer us.

The source code for this article is available on the Github repository.

Why should we care about requests?

First of all, Forerunners are the intermediate build between Implementers and Monads. They are more powerful than Enforcers but less powerful than Monads. The apps are also great for performing various context-free arithmetic operations such as parsers or traversable states.

In addition, all applicants are implementers, which may make implementing applications easier if you have some experience with implementers. Thanks to this relationship, applicants can make the entire journey from implementers to Monads easier, and play the role of bridge between both concepts.

Moreover, writing is usually easier than writing Monads. In more functional languages ​​such as Scala or Haskell, applicants tend to have more instances of Monads.

What is an application?

The applied port, or its acronym, is a mostly functional programming concept. It was introduced in 2008 by Conor McBride and Ross Patterson in their paper Applied Programming with Effects. Their indirect counterpart in category theory is known as indolent mononuclear afferents. In addition, please bear in mind that All applicants are executors. Such a relationship will have significant implications when we begin to discuss Applicable Laws and methods – mostly means that all the laws required by the representatives must also be adhered to by the implementers.

In the software world, the main focus of requests, similar to Monads, is to wrap values ​​in a specific context and then perform operations – to be precise, operations are encapsulated in the same context as the value. Unlike Monads, implementations do not allow sequential operations in the same ways that Monads allow, where the output of one process is the input of the other. Unfortunately, it can’t be easily implemented in Java, so we end up with that capability anyway. Moreover, unlike executors, applications allow us to serialize our accounts.

Applicable Laws

As both data types described earlier, applicants also have laws that must be met. In fact, implementers have the largest number of such laws, namely: identification, shape similarityAnd exchangeAnd to express. In my opinion, the applicable laws are also the most difficult to understand at first glance, in particular shape similarity And exchange, of all the three data types you described.

Classically, a few assumptions before we start:

  • F is an assignment function of type T To write s
  • g is an assignment function of type s To write WL
  1. identification
    Applying an identity function to a value wrapped in pure should always return an unchanging value.

    Applicative<Integer> identity = Applicative.pure(x).apply(Applicative.pure(Function.identity()));
    boolean identityLawFulfilled = identity.valueEquals(Applicative.pure(x));

  2. shape similarity
    Applying an encapsulated function to an encapsulated value should yield the same result as applying the function to the value and then wrapping the result with a 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. exchange
    Applying the wrapped function f to a wrapped value should be the same as applying the wrapping function, which provides 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. to express
    Applying the wrapping function f and then the wrapping function g should give the same results as applying the combination of the wrapped functions 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, since the implementer is an extension of the port, the instance must satisfy both canons of the port: identification And to express. Fortunately, both laws imposed by the definition of Functor are actually among the four applied laws, so they must be fulfilled by the same applied definition.

Now that I know the laws you have to fulfill, I can start talking about exactly what you need to fulfill your order.

Create an app

  1. What we need to implement the application

    The first thing you will need is a parameter type a . The parameterized type is the cornerstone of all data types that are similar in structure with implementers, Monads, and Funators. Moreover, you will need two methods:

    • Progressing (or AP) Responsible for carrying out operations. Here you pass a function that is already wrapped in our context and operates on the value in our context. This method should have the following signature M (M U>).
    • pure Which is used to encapsulate your value and has the following signature M (T).

    In addition, since all applicants are actors, you get a map way with signature M (T -> R) By definition.

    be cerfull:

    There is a second, perhaps more common, alternative to the application applied in such a case. Instead of an application method, we have a product method with signature M <(T ، U)> (M , M ). These applicants must comply with different laws, namely: associativeAnd left identity And correct identity. Their descriptions and examples are included in the GitHub repository.

    Knowing what one needs to place an order, I can start implementing it.

  2. Practical implementation

    public final class OptionalApplicative<T> implements Functor<T> {
    
        private final Optional<T> value;
    
        private OptionalApplicative(T value) {
            this.value = Optional.of(value);
        }
    
        private OptionalApplicative() {
            this.value = Optional.empty();
        }
    
        <U> OptionalApplicative<U> apply(OptionalApplicative<Function<T, U>> functionApplicative) {
            Optional<U> apply = functionApplicative.value.flatMap(value::map);
            return apply.map(OptionalApplicative::new).orElseGet(OptionalApplicative::new);
        }
    
        static <T> OptionalApplicative<T> pure(T value) {
            return new OptionalApplicative<>(value);
        }
    
        @Override
        public <R> Functor<R> map(Function<T, R> f) {
            return apply(pure(f));
        }
    
        // For sake of asserting in Example and LawsValidator
        public boolean valueEquals(Optional<T> s) {
            return value.equals(s);
        }
    
        public boolean valueEquals(OptionalApplicative<T> s) {
            return this.valueEquals(s.value);
        }
    }
    

    Above, you can see the application implementation ready, but why does it look the way it does?

  3. Description of the implementation

    The basis of this implementation is the parameterized class with the static field called “value”, which is responsible for storing the value. Then you can see the special constructors that make it impossible to create an object in any way other than the wrap method – pure.

    Next comes two unique approaches to applicants – pure Used to encapsulate values ​​in an application context and Progressing To use wrapped functions for wrapped values. Both are implemented using optionals and methods, so they are completely free and safe.

    Then you have a way a map Coming from Functor, it’s an “extra” method that might come in handy if you’re interested in doing some operation on the value inside the app.

  4. Example of application use

    Let’s move on to provide a simple example of an applied use with a short description of why it might be better than a non-applied approach.

    public class Example {
    
        public static void main(String[] args) {
            int x = 2;
            Function<Integer, String> f = Object::toString;
            // Task: applying function wrapped in context to value inside that context.
    
            // Non-applicative
            Optional<Integer> ox = Optional.of(x);
            Optional<Function<Integer, String>> of = Optional.of(f);
            Optional<String> ofx = ox.flatMap(d -> of.map(h -> h.apply(d)));
            // One liner -> Optional.of(x).flatMap(d -> Optional.of(f).map(h -> h.apply(d)));
    
            // Applicative
            OptionalApplicative<Integer> ax = OptionalApplicative.pure(x);
            OptionalApplicative<Function<Integer, String>> af = OptionalApplicative.pure(f);
            OptionalApplicative<String> afx = ax.apply(af);
            // One liner -> OptionalApplicative.pure(x).apply(OptionalApplicative.pure(f));
    
            if (afx.valueEquals(ofx)) {
                System.out.println("Values inside wrappers are equal");
            } else {
                throw new RuntimeException("Values inside wrappers are not equal");
            }
        }
    }

    Above you can see for yourself the potential benefits that applied abstraction provides over the optional based approach. First of all, the application-based code is simpler and easier to understand than the optional one, and it doesn’t require any complicated things like inline map calls. In fact, the user doesn’t even know they’re using Applicative with optional features, so I was able to provide better packaging. In my opinion, the pros of using such an abstraction outweigh the cost of writing one.

A summary of the above

Although a less powerful concept than Monads, apps can be a great solution especially when you need to deal with context-free computations. It’s very useful when you have to write dispensers or transit elements. Additionally, as they are the intermediate concept between implementers and Monads, they can ease your journey through category theory data types. thank you for your time.

Categories Theory Data Types in java

If this part of the text is interesting, you may also be interested in other articles from my series on class theory data types in Java:

  1. executors
  2. monad

Application Questions and Answers

  1. What is an application?

    An executor, or application, is a functional programming concept, all implementations are examples of actors.

  2. What are the applicable laws?

    Each applicable case must satisfy four laws: identificationAnd shape similarityAnd exchange, And to express

  3. What do I need to apply for an application?

    To implement an application, you need a parameterized type M And two ways: pure And Progressing, method ‘a mapInherited from Functor, so we get it by definition.

.

Leave a Comment