extractor付き述語を作る(Java)

この記事は:birthday:アイスタイル Advent Calendar 2020 2枠目:birthday:7日目の記事になります

こんにちは、sakohです。

今年のアドカレでは、できるだけ説明的なコーディングをテーマに2件ほど書きます。
こちらは1件目。
Javaで書いてますが、filter使うなら何の言語でもできるような話です。

やること

述語の取る主語とフィルタ対象が異なる時のために、
それを吸収するためのPredicate変換クラスを作ります。

……という説明で伝わる人は「もう作ってる」か、「作れるけど要らない」一方、
欲しいと思う可能性がある人には伝わりにくいので、さっさとつらみの例を挙げます。

JUnit5でUT書いてればextractingでピンとくるかもしれません。

述語の取る主語とフィルタ対象が異なる とは

値を持つクラス

簡単なサンプルのフィルタ対象として、こんなクラスがあるとします。
処理や意味が乗っていませんがValueObjectのなりかけですね。

  public static class WrappedInt {
    private final String name;
    private final int value;

    public static WrappedInt of(String name, int value) {
      return new WrappedInt(name, value);
    }

    private WrappedInt(String name, int value) {
      this.name = name;
      this.value = value;
    }

    public String name() {
      return this.name;
    }

    public int value() {
      return this.value;
    }
  }

それをfilterする

で、そのStreamなりなんなりをfilterしたいとします。
今回はintのラッパなのでその値ででも。

  {
    var wrappedInts =
        List.of(
            WrappedInt.of("a", 5),
            WrappedInt.of("e", 2),
            WrappedInt.of("i", 1),
            WrappedInt.of("o", 4),
            WrappedInt.of("u", 9));
    var evens =
        wrappedInts
            .stream()
            .filter(wrapped -> wrapped.value() % 2 == 0)
            .collect(toList());
  }

filterに与えた述語に注目します。

不都合というか座りがよくない

述語に型と名前を付けるとこうですね。

Predicate<WrappedInt> byHavingEvenValue = wrapped -> wrapped.value() % 2 == 0;

valueを取り出さなければならないがゆえに、一般的な

Predicate<Integer> isEven = value -> value != null && value % 2 == 0;

がそのまま使えず、
またごちゃごちゃした処理(Technical mumble-jumble)がにじみ出ていることで、

  • 述語を都度定義しなければならない面倒くささ
  • 述語自体の歯切れ悪さ

があります。
これが述語とフィルタ対象のずれです。

まあ筋力で毎回書けばそれで終わりな話なんですがそれじゃあ面白くないので。
面白いか面白くないかは大切なことですね?

なんとかする

extractorを別に用意する

まずは、そのプロジェクトにおいて広く評価可能な値を抽出(extract)してくる処理と
抽出された値を主語に取る述語を分けて考えます。

Function<WrappedInt, Integer> extractor = WrappedInt::value;
Predicate<Integer> isEven = value -> value != null && value % 2 == 0;

このようになりますね。
ただこれでmapしてしまうと元の値が返せませんので、
これを組み合わせてPredicateを出力してくれるクラスが欲しいです。

extractorとPredicate<抽出後>からPredicate<抽出前>を作るクラス

つまり、

public class By {
  private By() {}

  public static <O, F> That<O, F> having(Function<O, F> extractor) {
    return fPredicate -> o -> fPredicate.test(extractor.apply(o));
  }

  public interface That<A, B> extends Function<Predicate<B>, Predicate<A>> {

    @Override
    default Predicate<A> apply(Predicate<B> original) {
      return this.that(original);
    }

    Predicate<A> that(Predicate<B> bPredicate);
  }
}

こがん奴ですね。
havingはSQLのhaving句と合わせてます。
By.That#thatが返すPredicateは抽出前のオブジェクトに対するもので、
評価時にextractorが働くという仕組みです。

使ってみる

肝心の書き味ですが、このようになります。

  {
    var evens =
        wrappedInts
            .stream()
            .filter(By.having(WrappedInt::value).that(isEven))
            .collect(toList());
  }

filter by having value that is even

そこそこ書き心地も読み心地もよくなったのではないでしょうか。

おわりに

Javaは式より言葉で語る方の言語だと思います。
であれば、できるだけちゃんと語る方に寄せたいですね。
では。

発展

入れ子対応

extractorが多段階の場合にどう対応するかは是非考えてみてください。
List.ofのようにextractor2つ、3つの場合を実装する力技もこのさい悪くないと思いますし、
再帰でうまくやってもneatですね。

パターンマッチもどきとの組み合わせ

  Patterns<WrappedInt, String> pattern = // Function<WrappedInt, String>でもある
      patterns(
          when(having(WrappedInt::value).that(isEven),
              thenApply(WrappedInt::name).andThen(this.concat("は偶数"))),
          when(having(WrappedInt::value).that(isPositive),
              thenApply(WrappedInt::name).andThen(this.concat("は奇数の正数"))),
          orElse(
              thenApply(WrappedInt::name).andThen(this.concat("は奇数の負数"))));

こういうことに使っても噛み合います。
この場合Byは邪魔なのでstatic importしてますが、
さらにhasとかでシノニム書いてもよさそうですね。

……Patternsの方の話しろよって言われる気がしなくもないですが。

ところで

Predicateは「述語」という名詞です。
動詞とはアクセントが違う位置に来て、(objectなんかもそうですね)
プ↑レディカット、と読みます。ご存じでした?

飯を食い、やがて酒を飲むでしょう 2019/12中途入社のバックエンドエンジニア。 とは言いつつフロントエンドの作業もずっとしているので フォアシュトッパーあたり。 アイコンは https://commons.wikimedia.org/wiki/File:Glencairn_Glass-pjt.jpg より。