CodeGym /コース /JAVA 25 SELF /関数合成: compose と andThen

関数合成: compose と andThen

JAVA 25 SELF
レベル 49 , レッスン 1
使用可能

1. 関数合成の概念

少しだけ理論(退屈になりすぎない程度に)

数学における関数の合成とは、ある関数の結果を別の関数の入力として渡すことです。関数 fg があるとき、合成 g(f(x)) は「まず xf を適用し、その結果を g に渡す」ことを意味します。

プログラミングでも考え方は同じで、単純な関数を組み合わせて複雑な変換を作ります。なんでもかんでも1つの巨大な関数にせず、コードを柔軟で再利用可能、かつ読みやすくできます。

ケーキの製造ラインを想像してください。最初は生地(f)、次にクリーム(g)、最後にトッピング(h)。全体の流れは h(g(f(ingredients))) のようになります。

なぜ合成が重要なのか?

  • 合成により、プログラムを小さな「ブロック」関数から組み立てられます。
  • 再利用が容易: 1つの「ブロック」を重複なくさまざまな場所で使い回せます。
  • 柔軟性: ある工程を変えたいときは、その関数だけを差し替えればよく、他は触りません。
  • 可読性とテスト容易性: 小さな関数は読みやすく、検証や保守が簡単です。

2. インターフェース FunctioncomposeandThen メソッド

Function インターフェース: おさらい

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    // 合成のためのメソッド:
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
}
  • compose: compose に渡した関数が先に実行され、その後に現在の関数が実行されます。
  • andThen: 先に現在の関数が実行され、その後に andThen に渡した関数が実行されます。

ビジュアルな図解

// 2つの関数があるとします:
Function<String, Integer> parse = s -> Integer.parseInt(s);
Function<Integer, Integer> square = x -> x * x;

// compose: square.compose(parse) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25

// andThen: parse.andThen(square) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25

// ただし、型が異なる場合は順序が重要!

例: 文字列を数値に変換してから二乗

import java.util.function.Function;

public class ComposeAndThenDemo {
    public static void main(String[] args) {
        // 関数: 文字列を数値に変換
        Function<String, Integer> parse = s -> Integer.parseInt(s);

        // 関数: 数値を二乗
        Function<Integer, Integer> square = x -> x * x;

        // 組み合わせ: まずパースしてから二乗
        Function<String, Integer> parseThenSquare = parse.andThen(square);

        System.out.println(parseThenSquare.apply("7")); // 49

        // 逆にするとどうなる?
        // square.compose(parse) — この関数の組では同じ結果
        Function<String, Integer> squareOfParsed = square.compose(parse);

        System.out.println(squareOfParsed.apply("8")); // 64
    }
}

順序が重要になるのはいつ?

関数の型が一致しない場合、順序は極めて重要になります。例えば:

Function<String, String> addPrefix = s -> "User: " + s;
Function<String, Integer> length = s -> s.length();

Function<String, Integer> composed = addPrefix.andThen(length);
System.out.println(composed.apply("Alice")); // "User: Alice" -> 11

// ではこうすると?
// length.andThen(addPrefix) — コンパイルエラー!
// length は Integer を返し、addPrefix は String を受け取るため。

表: compose と andThen の違い

f
g
f.compose(g)
f.andThen(g)
T → R
V → T
V → R: f(g(x))
T → V: g(f(x))

3. Predicate など他のインターフェースの合成

Predicate<T>: andornegate

関数型インターフェース Predicate<T> は、boolean を返す関数です。Predicate を組み合わせるために次のメソッドが用意されています:

  • and: 論理積(&&
  • or: 論理和(||
  • negate: 否定(!

例: 複雑なフィルター条件

ユーザークラスがあるとします:

public class User {
    String name;
    int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

次に、いくつかの Predicate を定義します:

import java.util.function.Predicate;

Predicate<User> isAdult = user -> user.age >= 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");

// 組み合わせ: 成人 かつ 名前が「A」で始まる
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);

// 成人 または 名前が「A」で始まる
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);

// 非・成人
Predicate<User> notAdult = isAdult.negate();

これらの Predicate を使ってフィルタリングできます。たとえば Stream API で:

import java.util.List;
import java.util.stream.Collectors;

List<User> users = List.of(
    new User("Alice", 20),
    new User("Bob", 17),
    new User("Anna", 15),
    new User("Mike", 22)
);

List<User> filtered = users.stream()
    .filter(adultAndA)
    .collect(Collectors.toList());

// filtered に入るのは Alice だけ(成人かつ名前が「A」で始まる)

ConsumerFunctionSupplier の合成

  • Consumer<T>: メソッド andThen — 2つの操作を連続して実行できます。
  • Function<T, R>: 合成は上で説明しました。
  • Supplier<T>: 直接は合成できませんが、他の関数の内部で利用できます。

例: Consumer<T>.andThen

import java.util.function.Consumer;

Consumer<String> print = s -> System.out.println("受け取った: " + s);
Consumer<String> printUpper = s -> System.out.println("大文字: " + s.toUpperCase());

Consumer<String> combined = print.andThen(printUpper);

combined.accept("hello");
// 出力:
// 受け取った: hello
// 大文字: HELLO

4. 実践: 変換とフィルタのチェーン

課題1: Function の変換チェーンを作る

アプリケーションではユーザーを 「名前,年齢」 という文字列で保存しているとします。例: "Alice,20"。やること:

  • 文字列を User オブジェクトに変換する
  • 年齢を取り出す
  • ユーザーが成人かどうかを判定する
import java.util.function.Function;
import java.util.function.Predicate;

Function<String, User> stringToUser = str -> {
    String[] parts = str.split(",");
    return new User(parts[0], Integer.parseInt(parts[1]));
};
Function<User, Integer> getAge = user -> user.age;
Predicate<Integer> isAdultAge = age -> age >= 18;

// 組み合わせ: 文字列 -> User -> 年齢 -> Predicate
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);

String input = "Bob,19";
int age = stringToAge.apply(input);
System.out.println("年齢: " + age); // 19

System.out.println("成人ですか? " + isAdultAge.test(age)); // true

課題2: 複数の Predicate を組み合わせてフィルタリング

18歳より上で、かつ名前が「A」または「M」で始まるユーザーを選びたいとします。

Predicate<User> isAdult = user -> user.age > 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
Predicate<User> nameStartsWithM = user -> user.name.startsWith("M");

Predicate<User> filter = isAdult.and(nameStartsWithA.or(nameStartsWithM));

List<User> filtered = users.stream()
    .filter(filter)
    .collect(Collectors.toList());

課題3: 多段の Function 変換

やりたいこと: 文字列の前後の空白を削除し、すべて大文字にし、プレフィックス "USER: " を付ける。

Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> "USER: " + s;

// チェーンを作成
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);

System.out.println(pipeline.apply("   vasya   ")); // USER: VASYA

5. 関数合成でよくあるミス

エラー No.1: compose/andThen の順序を取り違える。
初心者はどちらが先に実行されるかをよく混同します。覚えておきましょう: f.compose(g) は「先に g、次に f」。f.andThen(g) は「先に f、次に g」です。

エラー No.2: 型が一致しない。
一方の関数の戻り型が、もう一方の関数の引数型と一致しない場合は合成できません。例えば、Function<Integer, String>.andThen(Function<Double, Boolean>) はできません。

エラー No.3: チェーンが複雑すぎる。
ビジネスロジックを1本のチェーンに詰め込みたくなることがありますが、結果として「スパゲッティ」になりがちです。小さな関数に分割し、わかりやすい名前を付けましょう。

エラー No.4: 関数に副作用がある。
関数や Predicate はできるだけ「純粋」(副作用なし)に保ちましょう。そうでないと、合成は危険で予測不能になりやすくなります。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION