Series overview
Part 7 of 2035% complete
2026-04-09•20 min read

Functors: mapping inside a context

You have been using a functor since Part 5 without naming it. A functor is any type F<T> that supports map — a way to apply a plain function T → R to the value inside the context while leaving the context itself alone:

Some(A) + (A -> B) = Some(B)
None + (A -> B) = None

The context (Some/None, Success/Failure, a list’s length) is structure; map promises to touch only the payload. That promise is what makes it safe to keep mapping.

The two laws

  • Identity: map(identity) = identity — mapping with the do-nothing function changes nothing.
  • Composition: map(g ∘ f) = map(g) ∘ map(f) — one fused map equals two maps in sequence.

The composition law is the one with practical teeth: it licenses merging two map calls into one (or splitting one into two) during refactoring, and it is the same guarantee that lets stream pipelines fuse intermediate operations.

Laws as tests

laws/OptionLawsTest.java
@Test
void functorIdentity() {
Option<Integer> some = Option.some(3);
Option<Integer> none = Option.none();
assertThat(some.map(Fn1.identity())).isEqualTo(some);
assertThat(none.map(Fn1.identity())).isEqualTo(none);
}
@Test
void functorComposition() {
Fn1<Integer, Integer> increment = x -> x + 1;
Fn1<Integer, String> show = Object::toString;
Option<Integer> some = Option.some(3);
assertThat(some.map(increment.andThen(show)))
.isEqualTo(some.map(increment).map(show));
}

The same two tests work verbatim for Result — and that is the real content of “functor”: not a Java interface (Java cannot express F<T> abstractly without higher-kinded types), but a contract any map-bearing type must honor.

Scheme example: DTO to domain facts

Registry responses arrive as DTOs — stringly-typed, shaped by the wire. The mapping into domain facts is a pure function, and map applies it inside whatever context the lookup produced:

scheme/CitizenMapper.java
public static CitizenFacts fromRegistry(CitizenRegistryDto dto) {
return new CitizenFacts(
dto.citizenId(),
"RESIDENT".equals(dto.residencyStatus()),
dto.annualIncome(),
dto.landVerified());
}
CitizenRegistry.java
Result<CitizenFacts, RegistryError> facts =
client.fetch(citizenId) // Result<CitizenRegistryDto, RegistryError>
.map(CitizenMapper::fromRegistry); // Result<CitizenFacts, RegistryError>

client here is the raw transport-facing CitizenRegistryClient — it returns wire-shaped DTOs, and the domain-facing CitizenRegistry from Part 6 is exactly this fetch-plus-map composition.

One line converts a raw registry response into validated domain facts, and a Timeout or NotFound passes through untouched — no try/catch, no null propagation, no separate error path to keep in sync.

map fromRegistry

Failure passes through

Result<CitizenRegistryDto, E>

Result<CitizenFacts, E>

map fromRegistry

Failure passes through

Result<CitizenRegistryDto, E>

Result<CitizenFacts, E>

Boundary worth naming: functors appear wherever a context wraps a value — Optional.map, Stream.map, CompletableFuture.thenApply. thenApply is map where the context is “not yet computed.” Once you see the pattern, the JDK’s API surface reorganizes itself into a small number of repeated shapes.

JavaFunctional Programming

Type to search the site.

↑↓ navigate⏎ openPowered by Pagefind