functional-reactive
Made in the European Union

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:

  1. Partial application โ€” fix some arguments early, vary the rest later. Useful for configurable filters, builder-like APIs, and dependency-light tests.
  2. 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; the x=3 layer 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

  • andThen chains left-to-right; compose chains right-to-left.
  • Type inference breaks on inline compose chains. Reach for higherCompose to fix it once.
  • Currying converts (A, B) โ†’ R into A โ†’ (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