Understanding Functional Programming Patterns
1. Understanding Pure Functions
| Property | Requirement |
| Determinism | Same input → same output |
| No side effects | No I/O, mutation, exceptions |
| Referential transparency | Call replaceable with result |
| Thread safety | Free by construction |
| Testability | No mocks needed |
2. Using Function Composition
| Combinator | Result |
f.andThen(g) | g ∘ f |
f.compose(g) | f ∘ g |
Function.identity() | Identity element |
Example: Pipeline
Function<String, String> clean = ((Function<String,String>) String::trim)
.andThen(String::toLowerCase)
.andThen(s -> s.replaceAll("\\s+", "-"));
clean.apply(" Hello World "); // "hello-world"
3. Understanding Higher-Order Functions
| Definition | Example |
| Takes function | list.forEach(Consumer) |
| Returns function | Function<A,B> build(Config c) |
| Both | map.compute(k, BiFunction) |
4. Using Currying and Partial Application
| Concept | Form |
| Curried 2-arg | Function<A, Function<B, R>> |
| Partial app | Bind one arg, return reduced-arity |
| vs BiFunction | Curried allows piecewise binding |
Example: Currying
Function<Integer, Function<Integer, Integer>> add = a -> b -> a + b;
Function<Integer, Integer> add5 = add.apply(5);
int n = add5.apply(3); // 8
5. Implementing Tail Recursion
| Aspect | JVM Status |
| Native TCO | Not supported by HotSpot |
| Workaround | Trampoline pattern with thunks |
| Alternative | Convert to iteration |
Example: Trampoline
interface Trampoline<T> {
T get(); default boolean done() { return true; }
default Trampoline<T> next() { throw new UnsupportedOperationException(); }
default T run() { Trampoline<T> t = this; while (!t.done()) t = t.next(); return t.get(); }
}
6. Using Memoization
| Technique | Notes |
ConcurrentHashMap.computeIfAbsent | Thread-safe cache |
| Caffeine | Bounded LRU + TTL |
| Suppliers.memoize (Guava) | Single-value lazy init |
Example: Generic memoize
static <K, V> Function<K, V> memoize(Function<K, V> f) {
var cache = new ConcurrentHashMap<K, V>();
return k -> cache.computeIfAbsent(k, f);
}
7. Understanding Lazy Evaluation
| Mechanism | Example |
| Stream | Intermediate ops deferred until terminal |
Supplier<T> | Defer computation until get() |
| Holder pattern | Lazy class init |
8. Working with Immutable Collections
| Factory | Result |
List.of(...) | Unmodifiable, no nulls |
Set.of(...) | Unmodifiable, no dups |
Map.of(k1,v1,...) | Up to 10 entries |
Map.entry() + Map.ofEntries() | Many entries |
List.copyOf(coll) | Defensive snapshot |
Collectors.toUnmodifiableList() | From stream |
9. Understanding Monads
| Monad in JDK | unit | flatMap |
Optional<T> | Optional.of | flatMap |
Stream<T> | Stream.of | flatMap |
CompletableFuture<T> | completedFuture | thenCompose |
Note: Monad laws: left identity, right identity, associativity. JDK's monad-like types satisfy these informally.
10. Implementing Map-Reduce Patterns
| Phase | Stream Op |
| Map | map / flatMap |
| Filter | filter |
| Reduce | reduce(identity, op) |
| Collect | collect(Collectors.X) |
Example: Word count
Map<String, Long> counts = Files.lines(path)
.flatMap(l -> Arrays.stream(l.toLowerCase().split("\\W+")))
.filter(w -> !w.isBlank())
.collect(Collectors.groupingBy(w -> w, Collectors.counting()));
11. Using Suppliers for Deferred Execution
| Use Case | API |
| Lazy log message | logger.atInfo().log(() -> "expensive: " + obj) |
| Default value | opt.orElseGet(() -> loadDefault()) |
| Lazy field | Holder + Supplier |
| Custom exception | orElseThrow(MyEx::new) |