Series overview
Part 2 of 2010% complete
2026-03-31•20 min read

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

core/Fn1.java
package in.o612.eng.jfp.core;
import java.util.Objects;
@FunctionalInterface
public 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

SchemeCodePipeline.java
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);

' farmer-support '

trim

toUpperCase

SchemeCode

' farmer-support '

trim

toUpperCase

SchemeCode

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:

core/Fn1Test.java
@Test
void 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:

core/Functions.java
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.

JavaFunctional Programming

Type to search the site.

↑↓ navigate⏎ openPowered by Pagefind