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) = NoneThe 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
@Testvoid 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);}
@Testvoid 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:
public static CitizenFacts fromRegistry(CitizenRegistryDto dto) { return new CitizenFacts( dto.citizenId(), "RESIDENT".equals(dto.residencyStatus()), dto.annualIncome(), dto.landVerified());}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.
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.