Composing functions
andThen, compose, currying โ and the type-inference traps that bite when you chain them inline.
May 17, 2026
Function, BiFunction and TriFunction are values. You can build them, store them, pass them around โ and combine them into new functions without touching their internals. This tutorial walks through the four building blocks you actually use day-to-day: andThen, compose, currying, and the helpers functional-reactive adds to dodge Java’s type-inference quirks.
andThen vs compose
Both methods on Function<A, B> produce a new function; the only difference is direction:
Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, Integer> plus3 = x -> x + 3;
times2.andThen(plus3).apply(5); // (5 * 2) + 3 = 13 โ left to right
times2.compose(plus3).apply(5); // 5 + 3 then * 2 = 16 โ right to left
a.andThen(b) reads naturally: “do a, then b.” a.compose(b) reads mathematically: “the composition of a after b.”
Use andThen for pipelines (the value flows through stages) and compose when you have a callee that expects a Function<X, Y> but you only have a Function<Y, Z> and a way to produce X โ Y.
The type-inference trap
andThen works fine when chained inline:
Function<Integer, Integer> f =
((Function<Integer, Integer>) x -> x * 2)
.andThen(x -> x + 3)
.andThen(x -> x - 1);
// fine: type is fixed by the first cast and flows through
compose does not, because each lambda extends the front of the chain:
Function<Integer, Integer> f =
((Function<Integer, Integer>) x -> x * 2)
.compose(x -> x + 3) // โ compile error: x is Object
.compose(x -> x - 1);
The first compose call has nothing earlier to anchor the type, so x is inferred as Object and x + 3 fails to compile.
Two ways out:
// 1. Cast each lambda explicitly
Function<Integer, Integer> f =
times2
.compose((Function<Integer, Integer>) x -> x + 3)
.compose((Function<Integer, Integer>) x -> x - 1);
// 2. Use higherCompose with explicit type arguments โ once
Function<Integer, Integer> g =
Transformations.<Integer, Integer, Integer>higherCompose()
.apply((Integer x) -> x + 3)
.apply((Integer x) -> x * 2);
higherCompose is compose reimagined as a curried function value. The explicit <Integer, Integer, Integer> on the call site fixes the types once for the whole chain โ no per-lambda casts.
Currying โ turn N-args into chained 1-args
A BiFunction<A, B, R> is equivalent to a Function<A, Function<B, R>>. The conversion is mechanical, and Transformations does it for you:
BiFunction<Integer, Integer, Integer> add = Integer::sum;
Function<Integer, Function<Integer, Integer>> curried =
Transformations.<Integer, Integer, Integer>curryBiFunction().apply(add);
curried.apply(2).apply(3); // 5
Function<Integer, Integer> add2 = curried.apply(2); // partial application
add2.apply(3); // 5
add2.apply(99); // 101
Why care? Two reasons:
- Partial application โ fix some arguments early, vary the rest later. Useful for configurable filters, builder-like APIs, and dependency-light tests.
- Memoization โ
Memoizer.memoize(BiFunction)internally curries so it can cache each level independently.Memoizer.memoize((x, y) -> heavy(x, y))on inputs(3, 4)and(3, 5)only pays the inner cost the second time; thex=3layer is reused.
The same pair (curryTriFunction / unCurryTriFunction) exists for 3-arg functions, and Checked-variants exist for both: curryCheckedBiFunction, unCurryCheckedTriFunction, etc.
A worked example: a configurable filter
You want a brand filter for cars. A first attempt:
Predicate<Car> bmw = car -> car.brand().equals("BMW");
Predicate<Car> vw = car -> car.brand().equals("VW");
That’s two predicates for two brands โ and ten predicates for ten brands. Lift the repetition into a function from String to Predicate<Car>:
Function<String, Predicate<Car>> brandFilter =
brand -> car -> car.brand().equals(brand);
brandFilter.apply("BMW").test(someCar); // boolean
Now you have one function that produces any brand filter. Compose it with StreamFunctions.streamFilter() and you can describe the whole pipeline before the stream even exists:
Function<String, Function<Stream<Car>, Stream<Car>>> brandSlice =
brandFilter.andThen(StreamFunctions.streamFilter());
brandSlice.apply("BMW")
.apply(carRepository.stream())
.forEach(System.out::println);
brandSlice is a value. Put it in a registry, hand it to tests, hand it to a UI component โ none of them need to know what Stream<Car> looks like yet.
Recap
andThenchains left-to-right;composechains right-to-left.- Type inference breaks on inline
composechains. Reach forhigherComposeto fix it once. - Currying converts
(A, B) โ RintoA โ (B โ R), unlocking partial application and per-level memoization. - Treating filters and pipelines as values lets you describe a whole workflow before you have the data.
Next
- Transformations API reference โ curry, uncurry, higherCompose, the
as*casting helpers. - Streams meet functions โ push the function-as-value idea into stream pipelines.
- Memoizing expensive computations โ currying’s most useful side effect.