java-recipes

ホーム 関数型プログラミング › Func-03

Func-03: 関数合成(andThen / compose)

複数の関数をつなげて1つの処理パイプラインを作る「関数合成」を解説します。andThen() は「f の後に g」(左から右)、compose() は「g を先に適用してから f」(右から左)という順序の違いを理解することが重要です。 また、Predicate の and()/or()/negate() で複数の条件を組み合わせる方法も学びます。

andThen と compose の違い

2つのメソッドは「どの順序で関数を適用するか」が逆になっています。 混乱しやすい箇所なので、具体的な例で確認しましょう。

メソッド適用順序数式で表すとイメージ
f.andThen(g)f を先に適用し、その結果に g を適用g(f(x))左から右(直感的)
f.compose(g)g を先に適用し、その結果に f を適用f(g(x))右から左(数学の合成関数と同じ)

Predicate の論理演算メソッド

  • p1.and(p2): p1 かつ p2 — 両方が true のとき true
  • p1.or(p2): p1 または p2 — どちらか一方が true のとき true
  • p.negate(): p の否定 — true と false を反転

サンプルコード

FunctionCompositionSample.java
import java.util.Arrays;
import java.util.List;
import java.util.function.*;
import java.util.stream.*;

public class FunctionCompositionSample {

    public static void main(String[] args) {
        System.out.println("=== Function.andThen(): f → g の順序合成 ===");
        Function<String, String> trim = String::trim;
        Function<String, String> toUpper = String::toUpperCase;
        Function<String, Integer> length = String::length;

        // trim してから toUpper
        Function<String, String> trimThenUpper = trim.andThen(toUpper);
        System.out.println(trimThenUpper.apply("  hello  ")); // HELLO

        // trim → toUpper → length の3段合成
        Function<String, Integer> pipeline = trim.andThen(toUpper).andThen(length);
        System.out.println(pipeline.apply("  hello  ")); // 5

        System.out.println("\n=== Function.compose(): g → f の逆順合成 ===");
        // compose は f.compose(g) = f(g(x)) つまり g を先に適用
        Function<String, String> upperThenTrim = toUpper.compose(trim);
        System.out.println(upperThenTrim.apply("  world  ")); // WORLD(trim してから upper)

        System.out.println("\n=== Predicate.and() / or() / negate() ===");
        Predicate<Integer> isPositive = n -> n > 0;
        Predicate<Integer> isEven = n -> n % 2 == 0;

        Predicate<Integer> isPositiveEven = isPositive.and(isEven);
        Predicate<Integer> isPositiveOrEven = isPositive.or(isEven);
        Predicate<Integer> isNotPositive = isPositive.negate();

        List<Integer> numbers = Arrays.asList(-4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6);

        System.out.print("正の偶数: ");
        numbers.stream().filter(isPositiveEven).forEach(n -> System.out.print(n + " "));
        System.out.println();

        System.out.print("正または偶数: ");
        numbers.stream().filter(isPositiveOrEven).forEach(n -> System.out.print(n + " "));
        System.out.println();

        System.out.print("正でない: ");
        numbers.stream().filter(isNotPositive).forEach(n -> System.out.print(n + " "));
        System.out.println();

        System.out.println("\n=== Consumer.andThen(): 複数の副作用を連結 ===");
        Consumer<String> log = s -> System.out.print("[LOG] " + s);
        Consumer<String> audit = s -> System.out.print(" [AUDIT] " + s);

        Consumer<String> logAndAudit = log.andThen(audit);
        logAndAudit.accept("処理完了");
        System.out.println();

        System.out.println("\n=== 実用例: バリデーションパイプライン ===");
        Function<String, String> trimFn = String::trim;
        Function<String, String> toLowerCase = String::toLowerCase;
        Predicate<String> notEmpty = s -> !s.isEmpty();
        Predicate<String> isEmail = s -> s.contains("@");

        Function<String, String> normalize = trimFn.andThen(toLowerCase);
        Predicate<String> isValidEmail = notEmpty.and(isEmail);

        List<String> inputs = Arrays.asList(" Test@Example.COM ", "invalid", "  ", "user@test.com");
        for (String input : inputs) {
            String normalized = normalize.apply(input);
            boolean valid = isValidEmail.test(normalized);
            System.out.println(input.trim() + " → " + normalized + " → valid=" + valid);
        }
    }
}

Java 8 で Function.andThen() と compose() が導入されました。andThen() は「f の後に g を適用」、compose() は「f.compose(g) = f(g(x)) で g を先に適用」です。Predicate には and()/or()/negate() があり、複数の条件を組み合わせられます。

よくあるミス・注意点

⚠️ andThen と compose で適用順が逆になる

f.andThen(g) は「f の後に g」ですが、f.compose(g) は「g の後に f」です。 たとえば toUpper.andThen(trim) は「大文字化してからトリム」ですが、toUpper.compose(trim) は「トリムしてから大文字化」と逆になります。 迷ったときは andThen() だけを使い、左から右の順に書くほうがわかりやすいです。

⚠️ Consumer.andThen() は副作用の連結であり、値を変換しない

Consumer.andThen() は Function と同じく 「前の Consumer の後に次の Consumer を実行する」という意味ですが、 Consumer は値を返さないため、各 Consumer は同じ入力値を受け取ります。 Function の andThen のように「前の結果を次に渡す」という動作とは異なる点に注意しましょう。

テストする観点

  • f.andThen(g)f.compose(g) で適用順が逆になること(同じ入力で異なる結果になるケースで確認)
  • 3段合成 f.andThen(g).andThen(h) が左から順に適用されること
  • Predicate.negate() で true/false が正しく反転されること(境界値: 判定がちょうど境界のとき)
  • Predicate.and() で片方だけ true のケース・両方 true のケース・両方 false のケースを網羅すること
  • バリデーションパイプラインで、正常なメール・空文字・@なし の各入力に対して期待通りの valid 値が返ること

GitHub でソースコードを見る →