Functions and composition
Composition is the cheapest powerful idea in this series. If f turns an A into a B and g turns a B into a C, then g ∘ f turns an A into a C. Everything that follows — map, flatMap, Kleisli arrows, traverse — is a variation on that one move, made to work when the B is wrapped in a context like Option or Result.
The laws, stated once
Two laws make composition safe:
- Identity:
identity ∘ f = f— composing with the do-nothing function changes nothing. - Associativity:
h ∘ (g ∘ f) = (h ∘ g) ∘ f— regrouping a pipeline never changes its meaning.
Associativity is the one you actually use daily: it is what lets you extract the middle of a pipeline into a named function without changing behavior.
Fn1, completed
package in.o612.eng.jfp.core;
import java.util.Objects;
@FunctionalInterfacepublic interface Fn1<T, R> { R apply(T value);
default <V> Fn1<T, V> andThen(Fn1<? super R, ? extends V> after) { Objects.requireNonNull(after, "after must not be null"); return value -> after.apply(apply(value)); }
default <V> Fn1<V, R> compose(Fn1<? super V, ? extends T> before) { Objects.requireNonNull(before, "before must not be null"); return value -> apply(before.apply(value)); }
static <T> Fn1<T, T> identity() { return value -> value; }}andThen reads left to right, matching the order the data flows; compose reads right to left, matching mathematical notation. They are each other with the arguments swapped — keep both because pipelines read better in andThen order and algebra reads better in compose order.
The example pipeline
Fn1<String, String> trim = String::trim;Fn1<String, String> normalizeCase = String::toUpperCase;Fn1<String, SchemeCode> toSchemeCode = SchemeCode::new;
Fn1<String, SchemeCode> normalizeSchemeCode = trim.andThen(normalizeCase).andThen(toSchemeCode);Each step is trivially testable on its own, and the assembled pipeline is testable as a unit. That is the payoff: three one-line functions, one reusable transformation, zero mocking.
Laws you can run
Laws are not decoration; they are regression tests for your mental model. AssertJ plus JUnit is enough:
@Testvoid compositionIsAssociative() { Fn1<Integer, Integer> f = x -> x + 1; Fn1<Integer, Integer> g = x -> x * 2; Fn1<Integer, Integer> h = x -> x - 3;
assertThat(h.compose(g.compose(f)).apply(10)) .isEqualTo(h.compose(g).compose(f).apply(10));}Currying, briefly
A BiFunction<A, B, C> takes two arguments at once. Currying rewrites it as a function that takes A and returns a function waiting for B:
public static <A, B, C> Fn1<A, Fn1<B, C>> curry(BiFunction<A, B, C> function) { Objects.requireNonNull(function, "function must not be null"); return a -> b -> function.apply(a, b);}Functions.curry(Integer::sum) gives you add.apply(2).apply(3) == 5. You will not curry everything — Java’s ergonomics fight you — but the trick is essential vocabulary for Part 11, where Kleisli composition chains functions that each produce a context.
Compared with the JDK: java.util.function.Function already has andThen, compose, and identity with identical semantics. Fn1 exists so the library speaks one vocabulary and so Part 9 can add monadic combinators to it. In application code that never touches jfp, plain Function is the right choice.