Applicative functor

Validation, parallel computation

Publish at:
    _    ____  ____  _     ___ ____    _  _____ _____     _______
   / \  |  _ \|  _ \| |   |_ _/ ___|  / \|_   _|_ _\ \   / / ____|
  / _ \ | |_) | |_) | |    | | |     / _ \ | |  | | \ \ / /|  _|
 / ___ \|  __/|  __/| |___ | | |___ / ___ \| |  | |  \ V / | |___
/_/___\_\_|_ _|_|_  |_____|___\____/_/__ \_\_| |___|  \_/  |_____|
|  ___| | | | \ | |/ ___|_   _/ _ \|  _ \/ ___|
| |_  | | | |  \| | |     | || | | | |_) \___ \
|  _| | |_| | |\  | |___  | || |_| |  _ < ___) |
|_|    \___/|_| \_|\____| |_| \___/|_| \_\____/

Feeling confident in your ability to transform data? Perhaps it seems you can take any instance of any type and apply any transformation you need. However, it is important not to be overconfident. While we have learned how to map one category to another (Functor), categories come in many forms, and there are situations where using only a functor becomes impractical.

For example:

A category where every object is a tuple (with whatever identity and composition is used). Mapping this category to another would be a matter of having a map function that takes a tuple, a transformer, and returns a transformed value.

map([a, b], f) => f([a, b])

So far, so good.

What if we need to apply map only to the first argument of a tuple? Also not a problem. Just take that first argument and transform it.

map([a, _], f) => f(a)

And another one.

What if that tuple is a function of one argument that returns a function of one argument:

g: x -> y -> z

In this case, our mapping looks like this

map(g, f) => f(...)

We have a problem, but not because functors fail completely. The issue is that the naive "map over a value" intuition is no longer enough. Once functions themselves are involved, we have to be precise about which type parameter is varying and how composition acts on outputs.

Arrow category illustrates why that shift in viewpoint matters. When morphisms themselves become objects, ordinary value-oriented intuition becomes awkward. That is what pushes us from plain functorial mapping toward structures that can express function application inside a context.

Applicability #

Arrow categories are categories in their own right. If F : C -> D is a functor between categories C and D, then it induces a functor Arr(C) -> Arr(D) by mapping arrows and commuting squares in C to arrows and commuting squares in D. That is a standard construction. For the programming intuition we need here, the key point is simpler: mapping over functions means composing another function onto their outputs.

And this is where we get our final clue.

Transforming a value in a context that represents a function is really applying function to a function, or applying a function to another function output, or just composing them.

And if you are curious to know how the functor for the function (->) looks like, that is simple.

instance Functor ((->) r) where
  fmap = (.)

In Haskell, the type constructor (->) r (functions from r) is a functor because you can "map" over the result of a function by composing another function with it.

This means:

  • fmap :: (a -> b) -> (r -> a) -> (r -> b)
  • Given a function g :: a -> b and a function f :: r -> a, fmap g f = g . f, which is a function r -> b.

Why is this a functor?

  • Identity law:

fmap id f = id . f = f

  • Composition law:
fmap (g . h) f =
(g . h) . f =
g . (h . f) =
fmap g (fmap h f)

(->) r is called the "reader functor" - it "reads" from an environment r and the result can be transformed as well.

fmap (+1) (*2) 10  -- = (+1) ((*2) 10) = (+1) 20 = 21

In TypeScript things are a bit more complicated. The compiler is not as expressive here as Haskell, but with a bit of help we can still model the same idea. Bear with me.

We can keep the same multi-step reasoning and still arrive at the same result.

  • Start with ordinary arrows.
type Arrow<A, B> = (a: A) => B;
  • A function functor does not treat Arrow<A, B> itself as the thing inside the functor. Instead, it fixes the input type and maps over the output.
  • So for a fixed input type R, the type constructor is:
type F<R, A> = Arrow<R, A>;
  • Now suppose we have:
const f: Arrow<number, string> = x => `${x} + 2`;   // A -> B
const g: Arrow<number, number> = x => x + 1;        // R -> A
  • To map f over g, we compose on the output side:
const composed: Arrow<number, string> = (r: number) => f(g(r)); // R -> B
  • Generalizing that gives us the function-functor map:
const fmap =
  <R>() =>
  <A, B>(f: Arrow<A, B>) =>
  (g: Arrow<R, A>): Arrow<R, B> =>
    (r: R) => f(g(r));

const add1: Arrow<number, number> = x => x + 1;
const add2: Arrow<number, string> = x => `${x} + 2`;

const result = fmap<number>()(add2)(add1);

console.log(result(5)); // '6 + 2'
  • This is exactly the same idea as Haskell fmap = (.):
fmap :: (A -> B) -> (R -> A) -> (R -> B)

The important point is that we got to the same result by fixing the input type R and mapping over outputs, not by treating Arrow<A, B> itself as a generic functor value.

using System;

class Program
{
    static void Main()
    {
        // fmap: (A -> B) -> (R -> A) -> (R -> B)
        Func<Func<A, B>, Func<R, A>, Func<R, B>> FMap<A, B, R>() =>
            (f, g) => x => f(g(x));

        Func<int, int> add1 = x => x + 1;
        Func<int, string> add2 = x => $"{x} + 2";

        var result = FMap<int, string, int>()(add2, add1);

        Console.WriteLine(result(5)); // prints: "6 + 2"
    }
}

From arrow functor to applicative functor #

It turns out that the idea of mapping over the arrow is useful by itself and spreads all over functional programming. It captures the computation and applies it in the context sequentially.

How about independent computations we want to combine in the same context? That is what applicative functor is for.[1]

An Applicative Functor allows you to apply functions inside a context. In many important cases it combines independent computations, and some instances can evaluate them in parallel.[2]

Formal definition #

An applicative functor is a structure within a category that extends the idea of a functor by enabling function application within a context.[3]

Before we go into a formal definition, we have to clear some things out. Just like functor definition relies on promise of something it operates with to be a category, applicative functor relies on the same and more. Effectively it extends a functor but in exchange makes assumptions more strict.[a]

For the purposes of this article, let C be a cartesian closed category, and let F: C -> C be an endofunctor.

An applicative functor is an endofunctor F with two additional operations:

  • pure: Id ⇒ F - a natural transformation, or pointwise pure_A : A -> F A
  • ap or <*>: F (A -> B) -> F A -> F B - applies a function inside the functor to a value inside the functor

We introduced two additional constraints:

  • Before we worked with general-purpose functors, and now we need an endofunctor. Here is the reasoning behind. Applicative needs to compose effects in the same category you want to be able to:

    • Take a value A in category C
    • Wrap it in a context F<A> - which also lies in C
    • Apply functions F<(A -> B)> to values F<A>, and get F<B> (arrow functor)

    All of this only makes sense if F(A), F(B), etc. are in the same category as A and B.

In other words: you can’t apply a function in F1(A -> B) to a value in F2(A) if F1 and F2 map to different categories - you’d be applying apples to oranges.

CCC Programming Concept
Terminal object void, unit
Product (A × B) Tuple (A, B)
Exponential (B^A) Function type A → B
Evaluation morphism Function application
(λf) Currying

Applicative functor laws #

Just like functor laws, applicative laws make sure underlying structure behaves consistently.

Applicative functor laws define how an applicative functor must behave to ensure consistent composition and function application in a context.[4]

Identity law #

Applying the identity function inside a context does nothing.

pure id <∗> v = v

Homomorphism Law #

Applying a pure function to a pure value yields a pure result

pure f <∗> pure x = pure(f x)

Interchange Law #

You can move the pure value to the function

u <∗> pure y = pure (\f -> f y) <∗> u

Composition Law #

Function application inside the context composes like ordinary function composition

pure (.) <∗> u <∗> v <∗> w = u <∗> (v <∗> w)

Examples #

TypeScript

  • We start with the general-purpose functor
  • Adding applicative like behavior, so it can hold values and not just computations.

Triangle applicative example #

Suppose we have a category of triangles

  • Objects - Triangles in the plane
  • Morphisms - Geometric transformations (rotations, translations, scalings, reflections, etc.)

An applicative functor F on this category assigns:

  • An object F(T) to each triangle T.
  • A morphism F(f): F(T) -> F(T') to each geometric transformation f: T -> T'.

For programming intuition, we also talk about extra applicative structure that lets us:

  • Lift any triangle T into the context F(T) using pure.
  • Treat a transformation as a value/function in context and apply it there with <*>.

Try to imagine a functor F that decorates a triangle with extra geometric data, for example:

  • F(T) is the original triangle T plus a shadow or outline shifted by a fixed vector.
  • F(f) applies the geometric transformation f to both the main triangle and its shadow.
  • pure embeds a triangle T into the decorated form F(T).
  • <*> expresses the programming intuition of applying a lifted geometric transformation to a decorated triangle, producing another decorated triangle.

Following the example step by step:

  • Start with a triangle T on the plane.
  • F(T) is the triangle plus its shadow shifted by (1, 0).
  • Let r : T -> T' be a rotation by 90 degrees.
  • F(r) applies rotation r to both T and the shadow.
  • Applicative structure lets us describe transformations on the decorated triangle in the same context.

Reader Applicative #

Remember parallel computation? So far, we have not seen it. We will fix that shortly, but first there is one more structure. Just like the arrow (->) is a functor, it is also an applicative.

With FArrow<R, A> we now have:

  • A computation depending on input R.
  • The ability to apply one function inside a context (fab) to another value inside the same context (fa).
  • Independent computations: they are not sequenced through one another, and they both read the same input r

In category theory, the Reader construction R -> A is most naturally viewed, in a cartesian closed category, as the exponential object A^R. In Set or Hask, this is concretely the function-space construction A ↦ (R -> A). Externally, it is represented by the hom-set Hom(R, A); internally, the endofunctor that stays inside the category is the exponential (-)^R.

So the Reader functor is best viewed categorically as the exponential endofunctor (-)^R, which maps:

  • Objects: A ↦ A^R (in Set/Hask: functions R -> A)
  • Morphisms: f: A -> B ↦ f^R: A^R -> B^R, which in Set/Hask is post-composition

The Reader Applicative extends this functor structure with additional categorical operations:

  1. Pure (Unit/Return): η: Id ⇒ (-)^R

    • Categorically: η_A: A -> A^R
    • In Set/Hask: η_A(a) = \_ -> a
  2. Apply: ⊛: (A -> B)^R × A^R -> B^R

    • In Set/Hask: (f ⊛ x)(r) = f(r)(x(r))
    • Categorically, this uses the evaluation morphism eval: (A -> B) × A -> B together with the diagonal morphism Δ: R -> R × R

What makes Reader Applicative categorically significant is that the cartesian closed structure gives exactly the ingredients needed for applicative behavior:

  • The diagonal morphism Δ: R -> R × R feeds the same environment to both branches
  • The evaluation morphism eval: (A -> B) × A -> B performs function application
  • The exponential structure (-)^R keeps the whole construction inside the category

The "parallel" shape of Reader Applicative comes from a categorical property: when we have f: R -> (A -> B) and x: R -> A, neither morphism depends on the result of the other. They only share the same domain R. That is independence of structure, not necessarily parallel execution in time.

Arrow vs Applicative #

Feature Function Functor ((->) r) Applicative ((->) r)
Models Mapping one computation's output by composition Combining computations that share one input
map Post-compose on outputs The same functorial map
<*> Not available from functor structure alone Apply a lifted function and a value at the same r
Reuses the same input r Yes, for a single computation r -> a Yes, and Δ : R -> R × R feeds both branches
newtype Reader r a = Reader { runReader :: r -> a }

instance Applicative (Reader r) where
    pure x = Reader (\_ -> x)
    Reader f <*> Reader x = Reader (\r -> f r (x r))

Reader means a computation that, given an input of type r (the environment), produces a value of type a. Applicative lets two such computations read the same input r and then combine their results.

When you write f <*> x, and both f and x are Reader r computations, they both receive the same environment r. They do not depend on one another's results; they are combined pointwise. This gives Reader the shape of independence, without implying actual parallel execution.

How about actual parallel computations?

import Control.Concurrent.Async (Concurrently(..))

-- Run two computations in parallel, then combine their results
parallelAdd :: IO Int
parallelAdd =
  runConcurrently $
    (+) <$> Concurrently (return 3)
        <*> Concurrently (return 4)

Here <$> and <*> combine two independent computations, and the Concurrently wrapper gives that applicative combination parallel semantics.

Reader in TypeScript:

Async Reader in TypeScript:

Reader in C#:

LINQ-Compatible Reader in C#:

CSV Transform example #

Our original CSV example could use some applicatives as well. For example to improve things like:

  • 🧪 Testability: Pass any Config to see different results.
  • 🔁 Composability: All transformations are composable pure functions.
  • 🌍 Configurability: Great for real-world apps with dynamic settings, user preferences, etc.
  • ♻️ Reusability: Each transformation can be reused with different contexts.

Visualizing applicatives #

0. Start with a functor F

A functor maps objects and morphisms while preserving structure:

Objects:   A, B              become   F(A), F(B)
Morphisms: f : A -> B        become   F(f) : F(A) -> F(B)

A ----f----> B
|            |
|     F      |
v            v
F(A)--F(f)-->F(B)


1. Add pure

Applicative adds a way to lift an ordinary value into the context:

pure_A : A -> F(A)

A -----------------> F(A)
      pure_A

Step-by-step:
  Step 1: Start with a value a : A
  Step 2: Lift it with pure_A
  Step 3: Get pure_A(a) : F(A)


2. Put a function in the context

Not only values can be lifted. Functions can be lifted too:

f : A -> B
pure f : F(A -> B)

A -> B -----------------> F(A -> B)
           pure


3. Apply inside the context

Applicative adds a second operation:

<*> : F(A -> B) × F(A) -> F(B)

This is the key difference from an ordinary functor.
A functor maps over F(A).
An applicative can also hold a function in the context and apply it there.

F(A -> B) -----------\
                     <*> --------> F(B)
F(A)  --------------/

Step-by-step:
  Step 1: Take u : F(A -> B)
  Step 2: Take v : F(A)
  Step 3: Combine them as u <*> v
  Step 4: Obtain a result in F(B)


4. Horizontal picture: Category 1 to Category 2

Category 1:                  Category 2:
Objects: A, B, A -> B  -->   Objects: F(A), F(B), F(A -> B)
Morphisms: f           -->   Morphisms: F(f)

A ------f------> B           F(A) -----F(f)-----> F(B)

                     pure
A -------------------------> F(A)

                     <*>
F(A -> B) × F(A)  ----------------------------->  F(B)


5. Vertical picture

The same idea can be read vertically as two layers:
the base category and the contextual one above it.

Category 1                Category 2
(base values)             (values in context F)

Objects: A, B             Objects: F(A), F(B), F(A -> B)
Morphisms: f              Morphisms: F(f)

      A                        F(A)
     / \                      /    \
    /   \                    /      \
   f     pure_A            pure_A    <*>
  /       \                 /       /   \
 /         \               /       /     \
B           ------------> F(A)  F(A -> B) × F(A)
                            \     /
                             \   /
                             F(B)


6. Geometric picture: triangles and transformations

Take a category where:

Objects:   triangles in the plane
Morphisms: geometric transformations
           (rotation, translation, scaling, reflection)

Rotation:

      /|                          ___
     / |      rotate 90 deg      \  |
    /__|     --------------->     \ |
                                   \|

Translation:

      /|                               /|
     / |       shift right            / |
    /__|     ----------------->      /__|

Scaling:

      /|                                /|
     / |        enlarge                / |
    /__|     ----------------->       /  |
                                     /   |
                                    /____|

Reflection:

      /|                               |\
     / |      reflect vertically       | \
    /__|     ------------------->      |__\

The applicative functor F decorates each triangle with extra geometric structure,
for example a shifted shadow or outline:

      /|
     / |\
    /__| \
    shadow

Borrowing programming notation for lifted transformations:

T  ---------------------> F(T)
          pure

F(T -> T') × F(T)  ------------->  F(T')
              <*>

Concrete reading:
  T            = one triangle
  T'           = the transformed triangle
  r : T -> T'  = a geometric transformation such as rotation or reflection
  F(T)         = triangle + shadow
  F(T')        = transformed triangle + transformed shadow
  F(r)         = apply the same transformation to both layers

Applicative picture:

Base geometry:

      /|                          ___
     / |      r : T -> T'        \  |
    /__|     --------------->     \ |
                                   \|

Applicative geometry:

      /|                             ___
     / |\      F(r) : F(T) -> F(T') \  |\
    /__| \   --------------------->  \ | \
                                      \|  \

The point of the applicative structure is that
the transformation is applied consistently inside
the context, not just to the bare triangle.


7. Reader-shaped intuition

When the context is "depends on the same input R",
the picture becomes:

u : R -> (A -> B)
v : R -> A

Applying them gives:

r -> u(r)(v(r)) : R -> B

         u
R -----------------> A -> B
\
 \
  \---------------> A
          v

Both branches read the same input R, then combine their results.

Conclusion #

Applicative functors are a powerful abstraction that allow function application within a context, enabling structured and context-aware computation and, for some effects, parallel composition.

Applicatives are ideal for:

  • ✅ Dependency injection — injecting a shared environment/config into multiple computations.
  • ✅ Shared context evaluation — such as logging, configuration, or user/session data.
  • ✅ Static analysis — since the structure of the computation is known up front.
  • ✅ Parallel composition — some independent effects can be executed concurrently if the effect supports it (e.g., with Task or Concurrently).

Use functors when you just want to map over a value in a context. Use applicatives when:

  • You have multiple inputs, each in a context (e.g., F<A>, F<B>, etc.).
  • You want to combine them using a function (e.g., F<(A, B) -> C>).

The computations are independent but share a common context.

Applicative programming style:

  • Describes structure (e.g., a form or data pipeline)
  • Keeps effects separate, but composed
  • Enables parallel execution if supported by the effect (e.g., Task)

Arrow vs Applicative:

  • Function functors such as ((->) r) model mapping by composition over one computation.
  • Applicative functors emphasize combining independent computations in a shared context.

Think of them as pure functions lifted into a context and combined without manually threading that context.

Source code #

Reference implementation (opens in a new tab)

Notes

  1. For the purposes of this article, assuming a cartesian closed category is stronger than the minimum needed for applicatives in general, but it matches the function-space examples used later. · Back

References

  1. Applicative functor (opens in a new tab) · Back
  2. Applying applicatives (opens in a new tab) · Back
  3. Functional Pearl - Applicative programming with effects (opens in a new tab) · Back
  4. Applicative functors (Wikibooks) (opens in a new tab) · Back