Magnus Smith
FP concepts are relevant in Java.
Functors and Monads can sound intimidating when unfamiliar.
Underlying ideas are powerful and useful.
Often, our values don't exist in isolation. They come with some form of "context":
Optional<T>
List<T>
or Stream<T>
Often, our values don't exist in isolation. They come with some form of "context":
CompletableFuture<T>
Either<L,R>
or
Try<T>
types.
How do we apply functions or transformations to the value(s) inside this context?
How do we avoid manually unpacking and repacking them every time?
A design pattern for types that contain value(s).
Provides a standard way to apply a function to the value(s) without changing the context.
It lifts a function into the world of contexts.
The transformation operation:
map
map
takes a
regular function A -> B
transforms Functor<A>
to Functor<B>
.
Imagine a sealed, transparent box containing a value
You have a tool that can transform the value.
The map
operation is like having robotic arms.
You give the arms the tool, and they reach inside the box, use the tool on the value
They place the transformed value into a new, identical sealed box.
You never opened the original box, and you get back the same type of box, just potentially holding a different value.
Structural context is preserved.
The function operates inside, and you get a new context back.
If the box was empty, the robot does nothing and returns a new empty box.
Optional<T>
Context: optionality
// Optional containing a String (context + value)
Optional<String> optName = Optional.of("Alice");
// Function: String -> Integer (length)
Function<String, Integer> nameLength = String::length;
// map applies the function *inside* the Optional context
// Input: Optional<String>, Function<String, Integer>
// Output: Optional<Integer>
Optional<Integer> optLength = optName.map(nameLength);
println(optLength); // Output: Optional[5]
// What if the Optional is empty?
Optional<String> emptyOpt = Optional.empty();
// Function is never called
Optional<Integer> emptyLength = emptyOpt.map(nameLength);
// Output: Optional.empty (context preserved)
println(emptyLength);
Stream<T>
Context: a sequence of elements
Stream<String> nameStream = Stream.of("Bob", "Charlie", "David");
// Function: String -> String (uppercase)
Function<String, String> toUpper = String::toUpperCase;
// map applies the function to each element *within* the Stream context
// Input: Stream<String>, Function<String, String>
// Output: Stream<String>
Stream<String> upperCaseStream = nameStream.map(toUpper);
upperCaseStream.forEach(System.out::println);
// BOB
// CHARLIE
// DAVID
For a type to be a well-behaved Functor, its map
should obey two laws:
1. Identity:
functor.map(x -> x)
should be equivalent to functor
.
2. Composition:
Mapping two functions sequentially is equivalent to mapping with the composition of those functions.
functor.map(f).map(g)
is equivalent to
functor.map(x -> g(f(x)))
map
What happens if the function we want to map
also returns a value wrapped in the same
context?
Nested contexts: Optional<Optional<String>>
map
// Function that looks up a userId and returns their User *as an Optional*
Optional<User> findUser(String userId) {
// ... logic to fetch user from database or return Optional.empty() ...
}
Optional<Address> findAddress(User user) {
// ... logic to fetch address, or return Optional.empty() if none ...
}
Optional<User> optUser = findUser(String userId);
// Nested contexts: `Optional` and `Optional`
Optional<Optional<Address>> nestedOptAddress
= optUser.map(this::findAddress);
A Monad is a Functor with additional features.
It's a pattern designed specifically to handle:
The key operation:flatMap
f: A -> Monad<B>
flatMap transforms
Monad<A>
to Monad<B>
.
Crucially, it maps then flattens the result, avoiding the nesting problem!
Lift a value into the context: of
In Java, typically static factory methods like
Optional.of()
, Stream.of()
.
Now, the function itself produces a new sealed box containing the result
f: User -> Optional<Address>
If you just used map
(the Functor way): Optional<Optional<Address>>
.
The flatMap
operation is smarter:
It effectively flattens Box<Box<Item>>
into just Box<Item>
.
Optional
Optional
is a Monad => flatMap
Optional<User> findUser(String userId) {
// ... logic to fetch user from database or return Optional.empty() ...
}
Optional<Address> findAddress(User user) {
// ... logic to fetch address, or return Optional.empty() if none ...
}
Optional<User> optUser = findUser(String userId);
// Use flatMap when the function returns an Optional!
// Input: Optional<User>, Function<User, Optional<Address>>
// Output: Optional<Address> (Flattened!)
Optional<Address> nestedOptAddress
= optUser.flatMap(this::findAddress);
flatMap
allows chaining operations that might return empty Optionals.
Stream
Stream
is a Monad => flatMap
// A list where each element is itself a list of Strings
List<List<String>> listOfLists = asList(asList("a", "b"),
asList("c", "d", "e"));
// Function: List<String> -> Stream<String> (returns a Stream - a Monad!)
Function<List<String>, Stream<String>> listToStream = List::stream;
// Using map creates a Stream of Streams (nested context)
// Output type: Stream<Stream<String>>
Stream<Stream<String>> nestedStream = listOfLists.stream().map(listToStream);
// Using flatMap flattens the result into a single Stream
// Input: Stream<List<String>>, Function<List<String>, Stream<String>>
// Output: Stream<String> (Flattened!)
Stream<String> flattenedStream = listOfLists.stream().flatMap(listToStream);
flattenedStream.forEach(s -> System.out.print(s + " ")); // Output: a b c d e
flatMap
is essential for flattening nested collections or streams.
flatMap
1. Left Identity:
Wrapping a value then flatMapping f
is the same as just applying f
to the value.
Monad.of(x).flatMap(f) <=> f(x)
flatMap
2. Right Identity:
FlatMapping with the wrapping function Monad::of
doesn't change the monad.
m.flatMap(Monad::of) <=> m
flatMap
3. Associativity:
How you group chained flatMap
calls doesn't matter.
m.flatMap(f).flatMap(g) <=> m.flatMap(x -> f(x).flatMap(g))
Core Idea:
Functor:
Mapping over a context.
Monad:
Sequencing/Chaining operations within a context
Key Method(s):
Functor:
map(A -> B)
Monad:
flatMap(A -> Monad<B>)
+ map(A -> B)
+ of(A -> Monad<A>)
Handles Function Returning...
Functor:
Plain value
Monad:
Contextual value Monad<B>
Result of applying Fn(A -> Context<B>)
Functor:
Context<Context<B>>
(via map
)
Monad:
Context<B>
(via flatMap
)
Analogy:
Functor:
Box + Tool (map)
Monad:
Smart Box + Tool that returns Box (flatMap)
All Monads are Functors, but not all Functors are Monads.
Cleaner Null Handling:
Optional.map
/flatMap
chains avoid deep nesting of if (x != null)
checks.
Code becomes more linear and readable.
Simplified Asynchronous Code:
CompletableFuture.thenApply
(map) / thenCompose
(flatMap).
chain async operations without complex callback management => "callback hell".
Declarative Data Pipelines:
Stream.map
/flatMap
build complex data transformations concisely and expressively.
Reduced Boilerplate:
Abstracting the "context handling" logic (checking for presence, iterating, handling async results) into reusable methods.
Improved Readability & Intent:
Code clearly expresses a sequence of operations or transformations.
Functors: Contexts you can map
over.
Monads: Functors you can also flatMap
over.
Common Java examples:
Optional
, Stream
, CompletableFuture
They provide structured ways to manage "context" (optionality, collections, async results).
Understanding them helps write cleaner, more robust, and more expressive Java code.