Applicative functor
Validation, parallel computation
_ ____ ____ _ ___ ____ _ _____ _____ _______
/ \ | _ \| _ \| | |_ _/ ___| / \|_ _|_ _\ \ / / ____|
/ _ \ | |_) | |_) | | | | | / _ \ | | | | \ \ / /| _|
/ ___ \| __/| __/| |___ | | |___ / ___ \| | | | \ V / | |___
/_/___\_\_|_ _|_|_ |_____|___\____/_/__ \_\_| |___| \_/ |_____|
| ___| | | | \ | |/ ___|_ _/ _ \| _ \/ ___|
| |_ | | | | \| | | | || | | | |_) \___ \
| _| | |_| | |\ | |___ | || |_| | _ < ___) |
|_| \___/|_| \_|\____| |_| \___/|_| \_\____/
Introduction #
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
mapfunction 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
maponly 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?
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 -> band a functionf :: r -> a,fmap g f = g . f, which is a functionr -> 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)
(->) ris called the "reader functor" - it "reads" from an environmentrand 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
foverg, 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.
For the purposes of this article, let C be a cartesian closed category, and let F: C -> C be an endofunctor. This is stronger than the minimum needed for applicatives in general, but it matches the function-space examples used later.
An applicative functor is an endofunctor F with two additional operations:
- pure:
Id ⇒ F- a natural transformation, or pointwisepure_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
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
Ain categoryC - Wrap it in a context
F<A>- which also lies inC - Apply functions
F<(A -> B)>to valuesF<A>, and getF<B>(arrow functor)
- Take a value
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 inF2(A)ifF1andF2map to different categories - you’d be applying apples to oranges.
And cartesian closed category provides us with the following:
| 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
example1 :: Maybe Int
example1 = pure id <*> Just 42
-- Result: Just 42
example2 :: [Int]
example2 = pure id <*> [1, 2, 3]
-- Result: [1, 2, 3]
const identity = <T>(x: T) => x;
const promiseValue = Promise.resolve("hello");
const promiseIdentity = Promise.resolve(identity);
// Simulate applicative ap
const result = promiseIdentity.then(f => promiseValue.then(f));
result.then(console.log); // logs: "hello"
const result2 = [(identity)].flatMap(f => [1, 2, 3].map(f));
console.log(result2);
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("=== Applicative Identity Law ===\n");
// Task<T> example (like Promise)
var taskResult = await TaskIdentityExample();
Console.WriteLine($"Task<T> result: {taskResult}"); // hello
// List<T> example (like Array)
var listResult = ListIdentityExample();
Console.WriteLine($"List<T> result: {string.Join(", ", listResult)}"); // 1, 2, 3
}
// Identity function
static T Identity<T>(T x) => x;
// -------------------------------
// 1. Task<T> (Promise-like)
// -------------------------------
static async Task<string> TaskIdentityExample()
{
var taskValue = Task.FromResult("hello");
var taskIdentity = Task.FromResult<Func<string, string>>(Identity);
return await Ap(taskIdentity, taskValue);
}
static async Task<TResult> Ap<T, TResult>(
Task<Func<T, TResult>> tf,
Task<T> tx)
{
var f = await tf;
var x = await tx;
return f(x);
}
// -------------------------------
// 2. List<T> (Array-like)
// -------------------------------
static List<int> ListIdentityExample()
{
var values = new List<int> { 1, 2, 3 };
var funcs = new List<Func<int, int>> { Identity };
return Ap(funcs, values);
}
static List<TResult> Ap<T, TResult>(
List<Func<T, TResult>> fs,
List<T> xs)
{
return fs.SelectMany(f => xs.Select(f)).ToList();
}
}
Homomorphism Law #
Applying a pure function to a pure value yields a pure result
pure f <∗> pure x = pure(f x)
add1 :: Int -> Int
add1 x = x + 1
-- Apply using applicative style
example1 = pure add1 <*> pure 2 -- Just 3
-- Apply directly
example2 = pure (add1 2) -- Just 3
example1 == example2
function main() {
console.log("=== Applicative Homomorphism Law ===\n");
const result = listHomomorphismExample();
console.log(`Array<T> result: ${result.join(", ")}`); // 6
}
// --------------------------------
// 1 Array<T> example
// --------------------------------
function listHomomorphismExample(): number[] {
const times2 = (x: number): number => x * 2;
// Left: applying array of functions to array of values
const left = [times2].flatMap(f => [3].map(f));
// Right: applying function directly to value, then wrapping in array
const right = [times2(3)];
console.log(`Left == Right? ${arraysEqual(left, right)}`); // true
return left;
}
// Helper to check shallow array equality
function arraysEqual<T>(a: T[], b: T[]): boolean {
return a.length === b.length && a.every((val, idx) => val === b[idx]);
}
main();
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("=== Applicative Homomorphism Law ===\n");
// 1. Task<T>
var result1 = await TaskHomomorphismExample();
Console.WriteLine($"Task<T> result: {result1}"); // 3
// 2. List<T>
var result2 = ListHomomorphismExample();
Console.WriteLine($"List<T> result: {string.Join(", ", result2)}"); // 6
}
// --------------------------------
// 1. Task<T> example (like Maybe)
// --------------------------------
static async Task<int> TaskHomomorphismExample()
{
Func<int, int> add1 = x => x + 1;
var left = await Ap(Task.FromResult(add1), Task.FromResult(2));
var right = await Task.FromResult(add1(2));
Console.WriteLine($"Left == Right? {left == right}"); // True
return left;
}
static async Task<TResult> Ap<T, TResult>(
Task<Func<T, TResult>> tf,
Task<T> tx)
{
var f = await tf;
var x = await tx;
return f(x);
}
// --------------------------------
// 2. List<T> example
// --------------------------------
static List<int> ListHomomorphismExample()
{
Func<int, int> times2 = x => x * 2;
var left = Ap(new List<Func<int, int>> { times2 }, new List<int> { 3 });
var right = new List<int> { times2(3) };
Console.WriteLine($"Left == Right? {left.SequenceEqual(right)}"); // True
return left;
}
static List<TResult> Ap<T, TResult>(
List<Func<T, TResult>> fs,
List<T> xs)
{
return fs.SelectMany(f => xs.Select(f)).ToList();
}
}
Interchange Law #
You can move the pure value to the function
u <∗> pure y = pure (\f -> f y) <∗> u
-- Define list of functions
u = [(+1), (*2)]
-- A pure value
y = 3
-- Left side of the law
left = u <*> pure y -- [4,6]
-- Right side of the law
right = pure (\f -> f y) <*> u -- [4,6]
-- Check equality
test = left == right -- True
function main() {
// List of functions
const u: Array<(x: number) => number> = [
x => x + 1,
x => x * 2
];
const y = 3;
// Left side: u <*> pure y
const left = u.map(f => f(y));
// Right side: pure (f => f(y)) <*> u
const right = [(f: (x: number) => number) => f(y)].flatMap(g => u.map(f => g(f)));
console.log("Left: ", left); // [4, 6]
console.log("Right: ", right); // [4, 6]
console.log("Equal? ", arraysEqual(left, right)); // true
}
function arraysEqual<T>(a: T[], b: T[]): boolean {
return a.length === b.length && a.every((v, i) => v === b[i]);
}
main();
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// List of functions (like [(+1), (*2)])
var u = new List<Func<int, int>> { x => x + 1, x => x * 2 };
// A pure value
int y = 3;
// Left side: u <*> pure y
var left = u.Select(f => f(y)).ToList();
// Right side: pure (f => f y) <*> u
var right = new List<Func<Func<int, int>, int>> { f => f(y) }
.SelectMany(g => u.Select(f => g(f)))
.ToList();
Console.WriteLine($"Left: {string.Join(", ", left)}");
Console.WriteLine($"Right: {string.Join(", ", right)}");
Console.WriteLine($"Equal? {left.SequenceEqual(right)}");
}
}
Composition Law #
Function application inside the context composes like ordinary function composition
pure (.) <∗> u <∗> v <∗> w = u <∗> (v <∗> w)
-- Define three lists
u = [(+1)] :: [Int -> Int]
v = [(*2)] :: [Int -> Int]
w = [10] :: [Int]
-- Left side: pure (.) <*> u <*> v <*> w
left = pure (.) <*> u <*> v <*> w -- [((+1) . (*2)) 10] = [21]
-- Right side: u <*> (v <*> w)
right = u <*> (v <*> w) -- [(+1) ((*2) 10)] = [21]
-- Test for equality
test = left == right -- True
function main() {
// u, v are arrays of functions; w is a value array
const u: Array<(x: number) => number> = [x => x + 1];
const v: Array<(x: number) => number> = [x => x * 2];
const w: number[] = [10];
// Function composition: (f ∘ g)(x) = f(g(x))
const compose = <A, B, C>(f: (b: B) => C, g: (a: A) => B) => (x: A) => f(g(x));
// Left: pure (.) <*> u <*> v <*> w
const left = [compose].flatMap(c =>
u.flatMap(f =>
v.flatMap(g =>
w.map(x => c(f, g)(x))
)
)
);
// Right: u <*> (v <*> w)
const right = u.flatMap(f =>
v.flatMap(g =>
w.map(x => f(g(x)))
)
);
console.log("Left: ", left); // [21]
console.log("Right:", right); // [21]
console.log("Equal?", arraysEqual(left, right)); // true
}
function arraysEqual<T>(a: T[], b: T[]): boolean {
return a.length === b.length && a.every((v, i) => v === b[i]);
}
main();
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// u = [(+1)]
var u = new List<Func<int, int>> { x => x + 1 };
// v = [(*2)]
var v = new List<Func<int, int>> { x => x * 2 };
// w = [10]
var w = new List<int> { 10 };
// Function composition: f ∘ g
Func<Func<int, int>, Func<int, int>, Func<int, int>> compose =
(f, g) => x => f(g(x));
// Left: pure (.) <*> u <*> v <*> w
var left = new List<Func<Func<int, int>, Func<int, int>, Func<int, int>>> { compose }
.SelectMany(c => u
.SelectMany(f => v
.SelectMany(g => w
.Select(x => c(f, g)(x))))).ToList();
// Right: u <*> (v <*> w)
var right = u
.SelectMany(f => v
.SelectMany(g => w
.Select(x => f(g(x))))).ToList();
Console.WriteLine($"Left: {string.Join(", ", left)}");
Console.WriteLine($"Right: {string.Join(", ", right)}");
Console.WriteLine($"Equal? {left.SequenceEqual(right)}");
}
}
Examples #
import Data.Functor.Identity
-- Values
result :: Identity Int
result = Identity 1
-- fmap chaining: fmap (+1) then fmap show with concatenation
mapped :: Identity String
mapped = fmap (\x -> show x ++ " + 1") (fmap (+1) result)
-- mapped == Identity "2 + 1"
-- Applicative apply: Identity (x -> x + 1) <*> Identity 1
applied :: Identity Int
applied = Identity (+1) <*> result
-- applied == Identity 2
TypeScript
- We start with the general-purpose functor
class F<A> {
constructor(public readonly value: A) {}
map<B>(f: (x: A) => B): F<B> {
return new F(f(this.value));
}
}
const result = new F<number>(1);
console.log(result.map(x => x + 1).map(x => `${x} + 1`)); // F { value: '2 + 1' }
- Adding applicative like behavior, so it can hold values and not just computations.
class F<A> {
constructor(public readonly value: A) {}
map<B>(f: (x: A) => B): F<B> {
return new F(f(this.value));
}
ap<B>(fab: F<(x: A) => B>): F<B> {
return new F(fab.value(this.value));
}
}
const result = new F<number>(1);
console.log(result.map(x => x + 1).map(x => `${x} + 1`)); // F { value: '2 + 1' }
console.log(result.ap(new F((x:number) => x + 1))) // F { value: 2 }
using System;
public class F<A>
{
public A Value { get; }
public F(A value)
{
Value = value;
}
// Functor map: (A → B) → F<A> → F<B>
public F<B> Map<B>(Func<A, B> f)
{
return new F<B>(f(Value));
}
// Applicative ap: F<Func<A, B>> → F<A> → F<B>
public F<B> Ap<B>(F<Func<A, B>> fab)
{
return new F<B>(fab.Value(Value));
}
public override string ToString()
{
return $"F {{ Value = {Value} }}";
}
}
class Program
{
static void Main()
{
var result = new F<int>(1);
var mapped = result
.Map(x => x + 1)
.Map(x => $"{x} + 1");
Console.WriteLine(mapped); // F { Value = 2 + 1 }
var applied = result.Ap(new F<Func<int, int>>(x => x + 1));
Console.WriteLine(applied); // F { Value = 2 }
}
}
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 triangleT. - A morphism
F(f): F(T) -> F(T')to each geometric transformationf: T -> T'.
For programming intuition, we also talk about extra applicative structure that lets us:
- Lift any triangle
Tinto the contextF(T)usingpure. - 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 triangleTplus a shadow or outline shifted by a fixed vector.F(f)applies the geometric transformationfto both the main triangle and its shadow.pureembeds a triangleTinto the decorated formF(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
Ton 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 rotationrto bothTand 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.
instance Applicative ((->) r) where
pure x = \_ -> x
f <*> g = \x -> f x (g x)
f :: Int -> String
f = show
g :: Int -> Bool
g = even
combinedApplicative :: Int -> String
combinedApplicative = (++) <$> (\x -> show (x + 1)) <*> (\x -> "-" ++ show (x * 2))
class FArrow<R, A> {
constructor(public readonly run: (r: R) => A) {}
static of<R, A>(a: A): FArrow<R, A> {
return new FArrow(() => a);
}
map<B>(f: (a: A) => B): FArrow<R, B> {
return new FArrow((r: R) => f(this.run(r)));
}
ap<B>(fab: FArrow<R, (a: A) => B>): FArrow<R, B> {
return new FArrow((r: R) => {
const func = fab.run(r); // function (a => b)
const val = this.run(r); // a
return func(val); // b
});
}
}
const fa = new FArrow((env: number) => env + 1);
const ff = new FArrow((env: number) => (x: number) => x * env);
const result = fa.ap(ff);
console.log(result.run(3)); // (3 + 1) * 3 = 12
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
using System;
public class FArrow<R, A>
{
private readonly Func<R, A> run;
public FArrow(Func<R, A> run)
{
this.run = run;
}
public A Run(R r) => this.run(r);
// Equivalent to FArrow.of(a)
public static FArrow<R, A> Of(A value)
{
return new FArrow<R, A>(_ => value);
}
// Functor map
public FArrow<R, B> Map<B>(Func<A, B> f)
{
return new FArrow<R, B>(r => f(this.run(r)));
}
// Applicative ap
public FArrow<R, B> Ap<B>(FArrow<R, Func<A, B>> fab)
{
return new FArrow<R, B>(r =>
{
var func = fab.Run(r); // Func<A, B>
var val = this.run(r); // A
return func(val); // B
});
}
}
class Program
{
static void Main()
{
var fa = new FArrow<int, int>(env => env + 1);
var ff = new FArrow<int, Func<int, int>>(env => x => x * env);
var result = fa.Ap(ff);
Console.WriteLine(result.Run(3)); // (3 + 1) * 3 = 12
}
}
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(inSet/Hask: functionsR -> A) - Morphisms:
f: A -> B ↦ f^R: A^R -> B^R, which inSet/Haskis post-composition
The Reader Applicative extends this functor structure with additional categorical operations:
-
Pure (Unit/Return):
η: Id ⇒ (-)^R- Categorically:
η_A: A -> A^R - In
Set/Hask:η_A(a) = \_ -> a
- Categorically:
-
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 -> Btogether with the diagonal morphismΔ: R -> R × R
- In
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 × Rfeeds the same environment to both branches - The evaluation morphism
eval: (A -> B) × A -> Bperforms function application - The exponential structure
(-)^Rkeeps 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))
Readermeans a computation that, given an input of typer(the environment), produces a value of typea. Applicative lets two such computations read the same inputrand 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:
// The Reader type: a function from environment R to value A
type Reader<R, A> = (r: R) => A;
class ReaderApplicative<R, A> {
constructor(public readonly run: Reader<R, A>) {}
// Functor: map :: (A -> B) -> Reader<R, A> -> Reader<R, B>
map<B>(f: (a: A) => B): ReaderApplicative<R, B> {
return new ReaderApplicative((r: R) => f(this.run(r)));
}
// Applicative: ap :: Reader<R, (A -> B)> -> Reader<R, A> -> Reader<R, B>
ap<B>(rf: ReaderApplicative<R, (a: A) => B>): ReaderApplicative<R, B> {
return new ReaderApplicative((r: R) => {
const func = rf.run(r);
const val = this.run(r);
return func(val);
});
}
// Applicative: pure :: A -> Reader<R, A>
static of<R, A>(value: A): ReaderApplicative<R, A> {
return new ReaderApplicative(() => value);
}
}
// Environment type
type Env = { multiplier: number };
// A Reader that uses the environment
const readerA = new ReaderApplicative<Env, number>(env => 5 + env.multiplier);
// A Reader that produces a function (Env -> A -> B)
const readerF = new ReaderApplicative<Env, (x: number) => number>(env => x => x * env.multiplier);
// Apply them together
const resultReader = readerA.ap(readerF);
// Run the computation with an environment
console.log(resultReader.run({ multiplier: 3 })); // (5 + 3) * 3 = 8 * 3 = 24
Async Reader in TypeScript:
// Async Reader: environment R -> Promise<A>
type ReaderAsync<R, A> = (r: R) => Promise<A>;
class ReaderT<R, A> {
constructor(public readonly run: ReaderAsync<R, A>) {}
// Functor: map
map<B>(f: (a: A) => B): ReaderT<R, B> {
return new ReaderT(async (r: R) => {
const a = await this.run(r);
return f(a);
});
}
// Applicative: ap (parallel async version)
ap<B>(rf: ReaderT<R, (a: A) => B>): ReaderT<R, B> {
return new ReaderT(async (r: R) => {
// Run both in parallel
const [func, val] = await Promise.all([rf.run(r), this.run(r)]);
return func(val);
});
}
// Applicative: pure
static of<R, A>(value: A): ReaderT<R, A> {
return new ReaderT(() => Promise.resolve(value));
}
}
// Environment type
type Env = { multiplier: number };
// Simulated async computation that adds to the multiplier
const readerA = new ReaderT<Env, number>(async (env) => {
await delay(100); // simulate latency
return 5 + env.multiplier;
});
// Simulated async computation that produces a function
const readerF = new ReaderT<Env, (x: number) => number>(async (env) => {
await delay(100); // simulate latency
return (x: number) => x * env.multiplier;
});
// Run both computations in parallel and apply result
const resultReader = readerA.ap(readerF);
// Run with environment
(async () => {
const result = await resultReader.run({ multiplier: 3 });
console.log(result); // (5 + 3) * 3 = 8 * 3 = 24
})();
// Helper delay function
function delay(ms: number): Promise<void> {
return new Promise(res => setTimeout(res, ms));
}
Reader in C#:
using System;
public class Reader<R, A>
{
public Func<R, A> run { get; }
public Reader(Func<R, A> run)
{
this.run = run;
}
// Run the computation with a given environment
public A Run(R env) => run(env);
// Functor: map :: (A -> B) -> Reader<R, A> -> Reader<R, B>
public Reader<R, B> Map<B>(Func<A, B> f)
{
return new Reader<R, B>(r => f(run(r)));
}
// Applicative: ap :: Reader<R, (A -> B)> -> Reader<R, A> -> Reader<R, B>
public Reader<R, B> Ap<B>(Reader<R, Func<A, B>> rf)
{
return new Reader<R, B>(r =>
{
var func = rf.run(r); // Func<A, B>
var val = run(r); // A
return func(val); // B
});
}
// Applicative: pure :: A -> Reader<R, A>
public static Reader<R, A> Pure(A value)
{
return new Reader<R, A>(_ => value);
}
}
class Env
{
public int Multiplier { get; set; }
}
class Program
{
static void Main()
{
var readerA = new Reader<Env, int>(env => 5 + env.Multiplier);
var readerF = new Reader<Env, Func<int, int>>(env => x => x * env.Multiplier);
var resultReader = readerA.Ap(readerF);
var result = resultReader.Run(new Env { Multiplier = 3 }); // (5 + 3) * 3 = 24
Console.WriteLine(result); // Output: 24
}
}
LINQ-Compatible Reader in C#:
using System;
public class Reader<R, A>
{
public Func<R, A> run { get; }
public Reader(Func<R, A> run)
{
this.run = run;
}
// Run the computation
public A Run(R env) => run(env);
// Functor: Select (map)
public Reader<R, B> Select<B>(Func<A, B> f)
{
return new Reader<R, B>(r => f(run(r)));
}
// Monad: SelectMany (bind / flatMap)
public Reader<R, B> SelectMany<B>(Func<A, Reader<R, B>> f)
{
return new Reader<R, B>(r =>
{
var a = run(r);
return f(a).run(r);
});
}
// For LINQ query with projection after SelectMany
public Reader<R, C> SelectMany<B, C>(Func<A, Reader<R, B>> f, Func<A, B, C> projector)
{
return new Reader<R, C>(r =>
{
var a = run(r);
var b = f(a).run(r);
return projector(a, b);
});
}
// Applicative pure
public static Reader<R, A> Pure(A value)
{
return new Reader<R, A>(_ => value);
}
}
class Env
{
public int Multiplier { get; set; }
}
class Program
{
static void Main()
{
var readerA = new Reader<Env, int>(env => 5 + env.Multiplier);
var readerB = new Reader<Env, int>(env => 10 * env.Multiplier);
// Using LINQ query syntax
var combined = from a in readerA
from b in readerB
select a + b;
var result = combined.Run(new Env { Multiplier = 3 }); // (5+3) + (10*3) = 8 + 30 = 38
Console.WriteLine(result); // Output: 38
}
}
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.
// Reader
type Reader<R, A> = (env: R) => A;
class ReaderApplicative<R, A> {
constructor(public readonly run: Reader<R, A>) {}
static of<R, A>(value: A): ReaderApplicative<R, A> {
return new ReaderApplicative(() => value);
}
map<B>(f: (a: A) => B): ReaderApplicative<R, B> {
return new ReaderApplicative(env => f(this.run(env)));
}
ap<B>(this: ReaderApplicative<R, (a: A) => B>, fa: ReaderApplicative<R, A>): ReaderApplicative<R, B> {
return new ReaderApplicative(env => this.run(env)(fa.run(env)));
}
}
// Config
type Config = {
ageThreshold: number;
locale: 'en' | 'de';
};
// Parsed row
type Row = Record<string, string>;
// Transformed row
type TransformerRow = {
original: Row;
age?: number;
ageGroup?: string;
name?: string;
};
// Step 1: toNumber
const toNumber: ReaderApplicative<Config, (row: Row) => TransformerRow> =
new ReaderApplicative(config => row => ({
original: row,
age: Number(row.age),
}));
// Step 2: addAgeGroup
const addAgeGroup: ReaderApplicative<Config, (row: TransformerRow) => TransformerRow> =
new ReaderApplicative(config => row => ({
...row,
ageGroup:
row.age == null
? 'young'
: row.age > config.ageThreshold
? 'old'
: 'young',
}));
// Step 3: localizeName
const localizeName: ReaderApplicative<Config, (row: TransformerRow) => TransformerRow> =
new ReaderApplicative(config => row => ({
...row,
name:
config.locale === 'de'
? row.original.name.toUpperCase()
: row.original.name.toLowerCase(),
}));
// ✅ Compose the transformations functionally
function transformDataWithReader(
data: Row[],
config: Config
): TransformerRow[] {
// Compose the functions inside the Reader context
const composed = toNumber.map(toNumFn =>
addAgeGroup.map(addGroupFn =>
localizeName.map(localizeFn => (row: Row): TransformerRow => {
const step1 = toNumFn(row);
const step2 = addGroupFn(step1);
const step3 = localizeFn(step2);
return step3;
}).run(config)
).run(config)
).run(config);
// Apply the composed function to each row
return data.map(row => composed(row));
}
function readCSV(data: string): Row[] {
const lines = data.trim().split(/\r?\n/);
if (lines.length < 2) return [];
const keys = lines[0].split(',').map(k => k.trim());
return lines.slice(1).map(line => {
const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
if (values.length !== keys.length) {
throw new Error(`Malformed row: "${line}"`);
}
return Object.fromEntries(keys.map((k, i) => [k, values[i]]));
});
}
const csvData = readCSV('name,age\nAlice,25\nBob,40');
const config: Config = {
ageThreshold: 30,
locale: 'de',
};
const transformed = transformDataWithReader(csvData, config);
console.log(transformed);
import Data.Char (toUpper, toLower)
import Control.Applicative (liftA3)
-- Custom splitOn (safe and total)
splitOn :: Eq a => a -> [a] -> [[a]]
splitOn delim xs = go xs [[]]
where
go [] acc = reverse (map reverse acc)
go (c:cs) (a:as)
| c == delim = go cs ([] : a : as)
| otherwise = go cs ((c : a) : as)
go _ [] = error "Unexpected empty accumulator in splitOn"
-- Configuration passed via Reader
data Config = Config
{ ageThreshold :: Int
, locale :: String
}
-- Output type
data Person = Person
{ name :: String
, age :: Int
, ageGroup :: String
} deriving (Show)
-- Per-row transformation using Reader
type Reader r a = r -> a
transformPerson :: String -> String -> Reader Config Person
transformPerson rawName rawAge = liftA3 Person
localizedName
parsedAge
grouped
where
parsedAge :: Reader Config Int
parsedAge = pure (read rawAge)
localizedName :: Reader Config String
localizedName cfg =
case locale cfg of
"de" -> map toUpper rawName
"en" -> map toLower rawName
_ -> rawName
grouped :: Reader Config String
grouped cfg =
let ageVal = read rawAge
in if ageVal > ageThreshold cfg then "old" else "young"
readCSV :: String -> Reader Config [Person]
readCSV csv =
case lines csv of
[] -> const [] -- return an empty Reader
(header : rows) ->
let keys = splitOn ',' header
parseRow row =
let values = splitOn ',' row
rowMap = zip keys values
mName = lookup "name" rowMap
mAge = lookup "age" rowMap
in case (mName, mAge) of
(Just n, Just a) -> transformPerson n a
_ -> error $ "Malformed row: " ++ row
in mapM parseRow rows
main :: IO ()
main = do
let csv = "name,age\nAlice,25\nBob,40"
let config = Config { ageThreshold = 30, locale = "de" }
let people = readCSV csv config
mapM_ print people
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
public class Reader<R, A>
{
public Func<R, A> run { get; }
public Reader(Func<R, A> run)
{
this.run = run;
}
public A Run(R env) => run(env);
// Functor: map
public Reader<R, B> Select<B>(Func<A, B> f)
{
return new Reader<R, B>(r => f(run(r)));
}
// Monad: bind
public Reader<R, B> SelectMany<B>(Func<A, Reader<R, B>> f)
{
return new Reader<R, B>(r =>
{
var a = run(r);
return f(a).run(r);
});
}
// Applicative: ap
public Reader<R, B> Ap<B>(Reader<R, Func<A, B>> rf)
{
return new Reader<R, B>(r =>
{
var f = rf.run(r);
var a = run(r);
return f(a);
});
}
// Static pure
public static Reader<R, A> Pure(A value) => new Reader<R, A>(_ => value);
}
class Program
{
public class Config
{
public int AgeThreshold { get; set; }
public string Locale { get; set; } = "en";
}
public class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
public string AgeGroup { get; set; } = "";
public override string ToString() =>
$"Person {{ Name = \"{Name}\", Age = {Age}, AgeGroup = \"{AgeGroup}\" }}";
}
// Reader class from above...
// Transformations as Readers returning Func<Person->Person>
static Reader<Config, Func<Person, Person>> ToNumber(string rawAge)
{
return Reader<Config, Func<Person, Person>>.Pure(p =>
{
p.Age = int.Parse(rawAge, CultureInfo.InvariantCulture);
return p;
});
}
static Reader<Config, Func<Person, Person>> AddAgeGroup()
{
return new Reader<Config, Func<Person, Person>>(config => p =>
{
p.AgeGroup = p.Age > config.AgeThreshold ? "old" : "young";
return p;
});
}
static Reader<Config, Func<Person, Person>> LocalizeName(string rawName)
{
return new Reader<Config, Func<Person, Person>>(config => p =>
{
p.Name = config.Locale == "de"
? rawName.ToUpperInvariant()
: rawName.ToLowerInvariant();
return p;
});
}
// Compose pipeline with Ap
static Reader<Config, Person> TransformPerson(string rawName, string rawAge)
{
var basePerson = Reader<Config, Person>.Pure(new Person());
var withName = basePerson.Ap(LocalizeName(rawName));
var withAge = withName.Ap(ToNumber(rawAge));
var withAgeGroup = withAge.Ap(AddAgeGroup());
return withAgeGroup;
}
// CSV parser
static List<Dictionary<string, string>> ReadCSV(string csv)
{
var lines = csv.Trim().Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
if (lines.Length < 2) return new List<Dictionary<string, string>>();
var headers = lines[0].Split(',').Select(h => h.Trim()).ToArray();
var rows = lines.Skip(1).Select(line =>
{
var values = line.Split(',').Select(v => v.Trim()).ToArray();
if (values.Length != headers.Length)
throw new Exception($"Malformed row: {line}");
return headers.Zip(values, (k, v) => new { k, v })
.ToDictionary(x => x.k, x => x.v);
}).ToList();
return rows;
}
static void Main()
{
var csv = "name,age\nAlice,25\nBob,40";
var data = ReadCSV(csv);
var config = new Config { AgeThreshold = 30, Locale = "de" };
var people = data.Select(row =>
{
var name = row["name"];
var age = row["age"];
return TransformPerson(name, age).Run(config);
}).ToList();
foreach (var person in people)
Console.WriteLine(person);
}
}
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)