【Javaクイズ】無理にラムダ使うな、使うなら格好良く使え

技術情報

この記事は、QiitaのJava Advent Calendar 2019 17日目の記事です。

問題

以下のようなenumがあります。

public enum FormatStyle {
    BOLD("b"), ITALIC("i"), UNDERLINE("u"), STRIKETHROUGH("s");

    private String tagName;

    FormatStyle(String tag) {
        this.tagName = tag;
    }

    public String format(String content) {
        // 例えばBOLDならば "<b>hoge</b>" と装飾する
        StringBuilder sb = new StringBuilder();
        sb.append("<").append(tagName).append(">");
        sb.append(content);
        sb.append("</").append(tagName).append(">");
        return sb.toString();
    }

    public static String decorate(EnumSet<FormatStyle> styles, String content) {
        return "???";
    }

複数のenum値を(EnumSetで)受け取り、文字列を装飾して返すdecorateメソッドを実装する必要があり、Aくんは以下のように実装しました。

    public static String decorateBad(EnumSet<FormatStyle> styles, String content) {
        final StringBuilder sb = new StringBuilder(content);

        styles.forEach(fs -> {
            String s = fs.format(sb.toString());
            sb.setLength(0); // StringBuilderをリセット
            sb.append(s);
        });

        return sb.toString();
    }

なんかちょっと残念な感じですね。あなたならどうリファクタリングしますか?

真っ当な解答

Aくんのコードの問題点は、無理にSetの内部イテレータを使用してラムダ式で処理を記述していることでした。
ラムダ式からスコープの外側の変数は参照できても、参照を書き換えることはできないため、途中の文字列の退避先としてStringBuiderオブジェクトを使用し、都度 setLenght(0) で中身をクリアしています。
これは内部イテレータの悪い使い方の典型例ですね。ラムダ式から外部のオブジェクトに副作用を引き起こすような行儀の悪いコードを書いてはいけません。
ということで以下のようにふつうに拡張for文で記述するのがよいでしょう。

    public static String decorateGood(EnumSet<FormatStyle> styles, String content) {
        String s = content;

        for (FormatStyle fs : styles) {
            s = fs.format(s);
        }

        return s;
    }

格好つけた解答

もしStream APIを使うことにこだわったなら、どのようなやり方があるでしょうか?
今回のケースでは、処理の順序性が重要なため、Collectorを使った処理はできません。reduceを使ってうまく処理を折り畳めないでしょうか?
以下が解答例となります。

   public static String decorateByComposedFunction(EnumSet<FormatStyle> styles, String content) {
        Function<FormatStyle, Function<String, String>> f = fs -> fs::format;

        return styles.stream()
                    .map(f::apply)
                    .reduce(Function.identity(), (fl, fr) -> fl.andThen(fr))
                    .apply(content);
    }

FormatStyle#format メソッドは、入力のStringオブジェクトに装飾を施して別のStringオブジェクトを出力します。即ち、String -> String の関数と見做すことができるでしょう。
実際にラムダ式を使用して Function<String, String>の関数オブジェクトを作成し、それらを合成した関数を実行(最後のapply)、というのが上記のコードのやっている内容です。
余談ですが、Streamの途中で難しいラムダを書こうとするとわけがわからなくなるので、上記コードのように一度ローカル変数に定義して参照すると、頭も見た目もスッキリします。

Function<FormatStyle, Function<String, String>> f = fs -> fs::format;

の部分は少し分かりづらいかもしれません。メソッド参照をラムダ式に戻して、以下のように書き直すと、理解しやすいかと思います。

Function<FormatStyle, Function<String, String>> f = fs -> (s -> fs.format(s));

FormatStyleオブジェクトを受け取って、「Stringオブジェクトを受け取ってStringオブジェクトを返す関数」を返す関数、ということになります。(2階の高階関数)

まとめ

中途半端にラムダやStreamを使って残念なコードを書くくらいなら、シンプルにfor文にしてしまいましょう。使うなら、格好良く、そしてわかりやすく書けるようにしましょう。
※よい別解がある場合は是非コメントください。

コメント

タイトルとURLをコピーしました