argius note

プログラミング関連

架空の拡張-filter,mapをStreamを使わずに組み込むとしたら

Java8では、Stream APIにより、コレクションの関数型ライクな操作ができるようになりました。
mapのような処理が1行で書けるようになってうれしい反面、もうちょっとすっきりしたのが良かったな、という不満もあります。


ただ不満を言っているだけの人になるのも嫌なので、「もし、こういう風に実装したならば、あるいは」というのを考えてみました。


おさらい - Java8でStreamでfilterとmapを使う

既存のコードとの兼ね合いを考えると、入力も出力もList<String>の場合が多いと思うので、その前提で。

List<String> input = Arrays.asList("Map", "List", "Collection", "Stream");
List<String> filtered = input.stream().filter(x -> x.length() >= 5).collect(Collectors.toList());
List<String> mapped = input.stream().map(String::toUpperCase).collect(Collectors.toList());

1行で書けるのはうれしいんですけど、1つのfilter/mapを使うために余分に3つものメソッド呼び出しを書かなければなりません。
Listfilter/mapをつけてくれたら良かったのに、と思いました。


関数型ライブラリーを使えば良いんじゃないの?

FunctionalJavaのような、可能な限りの関数型の表現を実現しようとするライブラリーがありますが、これは新たなクラス体系を作っています。
できれば、コアライブラリーのJCFをそのまま使って実現したいし、あと、関数型のレイヤーが厚すぎて拡張されたJavaになってしまうのは避けたい。
関数型じゃなくて、あくまで、コア機能に直接組み込んでもそれほど違和感が無い程度のfilter/mapだけで良いんです。


とにかく、Listfilter/mapだけで良いから、欲しい。


もし、標準ライブラリーに組み込むとしたら

java.util.ListStreamの上位に、Sequenceのようなインターフェイスがあると綺麗になりそうですが。
Sequenceを導入したら、Sequenceには実装をほとんど書かないで、NonLazySequencedefaultメソッドを書く、といった感じでしょうか。

  • Sequence(実在しない)
    • Stream
    • NonLazySequence(実在しない)
      • List

上の方法だと実験が大掛かりになってしまうので、java.util.Listだけに実装するようにして、極力、java.util.List以外を触らなくて良いように、defaultメソッドstaticメソッドを追加するだけで拡張できる方法を考えてみました。


以下のインターフェイスは、Listに継承させる前提で書いています。実際はListに直接追加しても良いかと思いますが、ここではコードを分割するためにも別のインターフェイスとしています。
なお、架空のコードなので、実在のクラス名にはYaという接頭辞を付けています。

import java.util.function.*;

interface ListFunctionalStyleOperationSupport<E> extends Iterable<E> {

    boolean add(E e);

    static <R> Supplier<? extends YaList<R>> getDefaultListConstructor() {
        return YaArrayList::new;
    }

    default YaList<E> filteredList(Predicate<E> pred) {
        return filteredList(getDefaultListConstructor(), pred);
    }

    default YaList<E> filteredList(Supplier<? extends YaList<E>> ctor, Predicate<E> pred) {
        YaList<E> a = ctor.get();
        for (E o : this)
            if (pred.test(o))
                a.add(o);
        return a;
    }

    default <R> YaList<R> mappedList(Function<E, R> mapper) {
        return mappedList(getDefaultListConstructor(), mapper);
    }

    default <R> YaList<R> mappedList(Supplier<? extends YaList<R>> ctor, Function<E, R> mapper) {
        YaList<R> a = ctor.get();
        for (E o : this)
            a.add(mapper.apply(o));
        return a;
    }

}

まず、filter/mapを実現するには、元のコレクションを複製する手段が必要になります。少なくとも、

  • 元のコレクションのすべての要素を並び順にイテレートできる
  • 元のコレクションと同じ型、もしくは互換性のある型のインスタンスを新たに生成する
  • 新たに生成したインスタンスに値を追加できる

の操作ができるようになっていなければなりません。

最初のイテレートは、Iterableを実装することで対応できます。Listの上位インターフェイスであるCollectionでもIterableを実装しているので、直接Listにコードを追加するなら不要です。
インスタンスの生成は、デフォルトではArrayList固定で。コンストラクター参照を使います。コンストラクターを指定したい場合は、引数が3のメソッドで、2番目にコンストラクター参照(たとえばLinkedList::new)を渡します。オーバーライドは必須ではありませんが、可能であればサブクラス自体のインスタンスを返すのが相応しいと思われます。
値を追加するのは、addメソッドで。これも、Collectionのものなので、直接Listにコードを追加するなら不要です。


あとは、filter/mapを書くだけ。ご覧のとおりです。
メソッド名がfilteredList/mappedListなのは、Javaでは継承階層の途中で自分自身の型を戻り値の型にすることができない*1ので、Sequenceがあった場合に名前がかぶってしまうのを避けるためです。
ちなみにScalaだと、オーバーライドしたメソッドの戻り値を変えることができるのでそれが可能になっています。(scala.collection.TraversableLikescala.collection.SeqLikeあたりを参照。)


実際にできる方法で

できないことばかりを言うのはこのくらいにして、現実的な解法を考えましょう。


ユーティリティーメソッドクラスを作ってしまうのが手っ取り早いでしょうか。

  • ユーティリティーメソッドクラスの例(FilterAndMapUtil
package lib;

import java.util.*;
import java.util.function.*;

public final class FilterAndMapUtil {

    // E: element type, R: mapped element type, T: mapped List type

    private FilterAndMapUtil() {
        //
    }

    public static <E> List<E> filter(List<E> src, Predicate<E> pred) {
        return filter(src, ArrayList::new, pred);
    }

    public static <E, T extends List<E>> T filter(T src, Supplier<T> ctor, Predicate<E> pred) {
        T a = ctor.get();
        for (E o : src)
            if (pred.test(o))
                a.add(o);
        return a;
    }

    public static <E> E[] filter(E[] src, Predicate<E> pred) {
        final int n = src.length;
        E[] a = Arrays.copyOf(src, n);
        int index = 0;
        for (int i = 0; i < n; i++)
            if (pred.test(src[i]))
                a[index++] = src[i];
        return Arrays.copyOf(src, index);
    }

    public static <E> List<E> map(List<E> src, Function<E, E> mapper) {
        return map(src, ArrayList::new, mapper);
    }

    public static <E, R, T extends List<R>> T map(List<E> src, Supplier<T> ctor, Function<E, R> mapper) {
        T a = ctor.get();
        for (E o : src)
            a.add(mapper.apply(o));
        return a;
    }

    public static <E> E[] map(E[] src, Function<E, E> mapper) {
        return map(src, Arrays.copyOf(src, src.length), mapper);
    }

    public static <E, R> R[] map(E[] src, IntFunction<R[]> ctor, Function<E, R> mapper) {
        return map(src, ctor.apply(src.length), mapper);
    }

    private static <E, R> R[] map(E[] src, R[] dst, Function<E, R> mapper) {
        assert (dst.length >= src.length);
        for (int i = 0; i < src.length; i++)
            dst[i] = mapper.apply(src[i]);
        return dst;
    }

    public static <E> List<E> filterMap(List<E> src, Function<E, Optional<E>> mapper) {
        return filterMap(src, ArrayList::new, mapper);
    }

    public static <E, R, T extends List<R>> T filterMap(List<E> src, Supplier<T> ctor, Function<E, Optional<R>> mapper) {
        T a = ctor.get();
        for (E o : src) {
            Optional<R> r = mapper.apply(o);
            if (r.isPresent())
                a.add(r.get());
        }
        return a;
    }

    public static <E> E[] filterMap(E[] src, Function<E, Optional<E>> mapper) {
        E[] a = Arrays.copyOf(src, src.length);
        final int newSize = filterMap(src, a, mapper);
        return Arrays.copyOf(a, newSize);
    }

    public static <E, R> R[] filterMap(E[] src, IntFunction<R[]> ctor, Function<E, Optional<R>> mapper) {
        R[] a = ctor.apply(src.length);
        final int newSize = filterMap(src, a, mapper);
        return Arrays.copyOf(a, newSize);
    }

    private static <E, R> int filterMap(E[] src, R[] dst, Function<E, Optional<R>> mapper) {
        assert (dst.length >= src.length);
        int index = 0;
        for (int i = 0; i < src.length; i++) {
            Optional<R> v = mapper.apply(src[i]);
            if (v.isPresent())
                dst[index++] = v.get();
        }
        return index;
    }

}

ここでは、filterMapと、配列向けのオーバーロードメソッドも作ってみました。


  • 使い方
// import static lib.FilterAndMapUtil.*;

List<String> stringList = Arrays.asList("Map", "List", "Collection", "Stream");
String[] strings = { "Map", "List", "Collection", "Stream" };

List<String> filtered = filter(stringList, x -> x.length() >= 5);
List<String> mapped = map(stringList, String::toUpperCase);
Integer[] lengths = map(strings, Integer[]::new, x -> x.length());

あまりスマートとは言えないやり方ですが、少なくとも私にとっては、配列かListfilter/mapができれば事足りるので、これで十分です。



おわりに

なんにせよ、ラムダ式インターフェイスのデフォルトメソッド(static含む)が使えるようになったのは大きいですね。

*1:分かりやすく説明するのが難しい。継承関係が親子1世代しかない場合ならできます。