Stream API の代わりに

Eclipse Collections を

使ってみた話

#jjug_ccc #ccc_m7

Nov. 18, 2017

ToC

  1. 前提のお話
  2. Eclipse Collections とは
  3. Eclipse Collections の API を見る
  4. 使ってみた所感
    1. 便利ポイント
    2. 困りポイント

whoami

  • Nagahori Shota
  • Twitter: @nashcft
  • Software developer @ U-NEXT
    • Server-side API developer (May. 2016 ~ Aug. 2017)
    • Android app developer (Sep. 2017 ~)

お話の前に

  • お話のレベル感としては使い方がどうとか周辺情報とか
  • ライブラリ自体の実装の話とか込み入った話はしない
  • Java 8 からの機能の話が当たり前のように出てくる
    • ラムダ式は使用禁止とか変態ワザとか仰る怖い方が世の中にはいらっしゃるようなので...
    • Java 9 もリリースされたしもういいよね...?
    • そういえばこのスライドでは Lambda を雑に「関数」と呼んだりします

Eclipse Collections とは

Basic Information

Brief History

  • 2004: Goldman Sachs によって社内向けに開発
  • 2012: GS Collections としてOSS化
  • 2015: Eclipse Foundation に移管, Eclipse Collections に改名

Latest Version

Committer

Eclipse Collections の API を見る

そういうのいいから早く触りたい

まあまあそういうこと言わずに

とりあえず interface を眺める

とりあえず初期化する

  • それぞれのコンテナに対するFactoryクラスがある
  • <コンテナの種類>.<性質> で具体的なコンテナが決まる
    • e.g.) Lists.immutable => ImmutableList
  • of / with で要素を放り込む
    • e.g.) Lists.immutable.of("apple", "orange", "banana")
  • ofAll / withAll にコンテナを与えると変換してくれる
    • e.g.) Lists.mutable.ofAll(Arrays.asList(1, 2, 3, 4))

とりあえず初期化する

// of/with (どっちも同じ) 
ImmutableList<Integer> list 
            = Lists.immutable.of(1, 2, 3, 4); // => [1, 2, 3, 4]

MutableSet<String> set
            = Sets.mutable("a", "b", "c", "b", "e"); 
            // => ("a", "b", "c", "e")

FixedSizeMap<String, Integer> map
            = Maps.fixedSize.of("one", 1, "two", 2, "three", 3);
            // => {"one": 1, "two": 2, "three": 3}

// ofAll/withAll
ImmutableMap<String, Integer> map2 
            = Maps.immutable.ofAll(someStringIntegerMap);
            // note: Map 系は ImmutableMap しか ofAll/withAll を持たない

MutableList<Foo> list2
            = Lists.mutable.ofAll(fooIterable);
           // note: List 系の ofAll/withAll は Iterable ならなんでもOK

collect

  • コンテナのそれぞれの要素に対して、引数に与えた関数を適用して要素を変化させる
    
   [🙂, 😉, 😛, 🙁]
    |   |   |   |
 { collect: 🙂 -> :) }
    |   |   |   |
    v   v   v   v
   [:), ;), :p, :(]

select

  • コンテナのそれぞれの要素に対して、引数に与えた boolean を返す関数を適用して、true となる要素のみを含めたコンテナを返す
    
    [🙂, 😉, 😤, 🙁]
     |   |   |   |
{ select: 🙂 -> smiling? }
     |   |   x   x
     v   v
    [🙂, 😉       ]

reject

  • コンテナのそれぞれの要素に対して、引数に与えた boolean を返す関数を適用して、false となる要素のみを含めたコンテナを返す (select の逆)
    
    [🙂, 😉, 😤, 🙁]
     |   |   |   |
{ reject: 🙂 -> smiling? }
     x   x   |   |
             v   v
    [        😤, 🙁]

collect / select / reject

// collect
// Eclipse Collections
Lists.mutable.of(1, 2, 3, 4) 
        .collect(n ->  n + 1);    // => [2, 3, 4, 5]
// Stream API で対応するもの: Stream::map
Arrays.asList(1, 2, 3, 4).stream()
        .map(n -> n + 1)
        .collect(Collectors.toList());

// select
// Eclipse Collections
lists.mutable.of(1, 2, 3, 4)
        .select(n -> n % 2 == 0); // => [2, 4]
// Stream API で対応するもの: Stream::filter
Arrays.asList(1, 2, 3, 4).stream()
        .filter(n -> n % 2 == 0)
        .collect.(Collectors.toList());

// reject
lists.mutable.of(1, 2, 3, 4)
        .reject(n -> n % 2 == 0); // => [1, 3]

injectInto

  • 初期値を用意して、それとコンテナの各要素を使う関数を適用して1個の値にする

         [🌶, 🥕, 🥔, 🌰, 🍖, 🍚]
          |   |   |  |   |   |
{ injectInto: (🍽, (a, e) -> a + 🔪(e)) }
                    |
                    v
                    🍛

injectInto

// Eclipse Collections
Lists.immutable.of(1,2,3,4)
        .injectInto(0, (a, b) -> Integer.sum(a, b)); // => 10

// Stream API で対応するもの: Stream::reduce
// reduce() の返却値は Optional<T>
Arrays.asList(1,2,3,4).stream()
        .reduce((a, b) -> a + b).get();

// Eclipse Collections も 8.0 から 
// Stream API のと同じ挙動の reduce() が実装された
Lists.immutable.of(1,2,3,4)
        .reduce((a, b) -> Integer.sum(a, b)).get();

その他の終端処理

// allSatisfy: コンテナが持つ要素全てが引数に与えた条件を満たす場合 true を返す
Lists.mutable.of("Java", "Scala", "Clojure", "Groovy")
                    .allSatisfy(lang -> runsOnJVM(lang));    // => true

// anySatisfy: コンテナが持つ要素のどれか1つでも
//             引数に与えた条件を満たす場合 true を返す
Lists.mutable.of("Io", "Erlang", "Eta", "Golang")
                    .anySatisfy(lang -> runsOnJVM(lang));    // => true

// getFirst: コンテナが持つ最初の要素を返す
Lists.mutable.of(1, 2, 3, 4).getFirst(); // => 1

// detect: コンテナが持つ要素の内、引数に与えた条件を満たす最初のものを返す
//         select().getFirst() と同じ
Lists.mutable.of(1, 2, 3, 4).detect(n -> n % == 0); // => 2

// などなど...

flatCollect

  • 引数に与えた関数を適用する、というところは collect と同じ
  • flatMap は変化させた結果同じ型のコンテナの入れ子になる場合のみ使用可能で、入れ子構造を外す
              [🙂, 😉, 😛, 🙁]
               |   |   |   |
        { flatCollect: 🙂 -> [...] }
               |   |   |   |
               v   v   v   v
[[🙂, 😀, 😇], [😉, 😜], [😛, 🤑], [🙁, 😠, 😡]]
               |   |   |   |
               v   v   v   v
    [🙂, 😀, 😇, 😉, 😜, 😛, 🤑, 🙁, 😠, 😡]

flatCollect

  • e.g.) あるシリーズ物の映像作品の全作品を視聴する場合の総再生時間を求める
    • とりあえず Title のリストがあるとする
# 雑なデータ構成の説明

[Series]
    -(1..n)-[Title]
               -(1..n)-[Episode]
                         ↑ 再生時間持ってる

flatCollect

public class Title {

    // ...

    public ImmutableList<Episode> getEpisodes() {
        return episodes;
    }
}

public class Episode {

    // ...

    public long getDuration() {
        return duration;
    }
}
  • 雑な感じでこういうものがあるとする

flatCollect

// collect だけで総再生時間を求めようとすると...
// ImmutableList<Title> titles;
titles.collect(Title::getEpisodes) // ImmutableList<ImmutableList<Episode>>
        .collect(episodes -> episodes
                          .collect(Episode::getDuration)
                          .injectInto(0, Long::sum)) // ImmutableLongList
        .injectInto(0, Long::sum); // long

とても辛い実装ですね?

 

※ <class>::<method> はメソッド参照という怠惰な人間のための記法
Tittle::getEpisodes だったら (title -> title.getEpisodes()) と同じ意味になる

flatCollect

// flatCollect で総再生時間を求めようとすると...
// ImmutableList<Title> titles;
titles.flatCollect(Title::getEpisodes) // ImmutableList<Episode>
        .collect(Episode::getDuration) // ImmutableLongList
        .injectInto(0, Long::sum); // long

// Stream API では flatMap
// こっちの flatMap は Stream に合わせるので以下のようになる
titles.stream()
        .flatMap(title -> title.getEpisodes().stream())
        .map(Episode::getDuration)
        .reduce(0, Long::sum); // reduce() も初期値を受けるやつがある

flatCollect(Title::getEpisodes) のところで ImmutableList の入れ子が取れてその後の記述がスッキリした

Aggregation

  • Map の形に要素を集約する
  • aggregateBy  と aggregateInPlaceBy がある
  • シグネチャの見た目がヤバい
    • 第一引数が key を作る関数、第二引数が value のデフォルト値を作る関数、第三引数が value を作成する関数
aggregateBy​(Function<? super T,? extends K> groupBy, 
            Function0<? extends V> zeroValueFactory, 
            Function2<? super V,? super T,? extends V> nonMutatingAggregator);

aggregateInPlaceBy​(Function<? super T,? extends K> groupBy,
                   Function0<? extends V> zeroValueFactory,
                   Procedure2<? super V,? super T> mutatingAggregator);

// Function<T, V> は T型の値を引数にV型の値を返す関数インターフェイス
// Function0<R> は引数なしにR型の値を返す関数インターフェイス
// Function2<T1, T2, R> はT1, T2型の値を引数にR型の値を返す関数インターフェイス
// Procedure2<T1, T2> は T1, T2型の値を引数にとる返り値なしの関数インターフェイス

Aggregation

// aggregateBy
Lists.immutable.of(1, 2, 3, 4, 5, 6, 7)
        .aggregateBy(
                n -> (n % 2 == 0) ? "even" : "odd",
                () -> Lists.mutable.empty(),
                (accum, n) -> {
                    accum.add(n);
                    return accum;
                }
        );
// => {"even": [2, 4, 6], "odd": [1, 3, 5, 7]}

// aggregateInPlaceBy
Lists.immutable.of(1, 2, 3, 4, 5, 6, 7)
        .aggregateBy(
                n -> (n % 2 == 0) ? "even" : "odd",
                () -> Lists.mutable.empty(),
                (accum, n) -> {
                    accum.add(n); // <= return しないで accum を変更する
                }
        );

Lazy Evaluation

  • Eclipse Collections はデフォルトで逐次実行
    • コンテナ全体に対して順番にメソッドを実行する
  • コンテナに対して asLazy() を記述してから操作を記述していくと処理が遅延化される
    • コンテナの要素1つずつに対して一連の操作を適用する
    • select() などで最終的な要素数が減る場合、最初から結果に必要な要素に対してしか操作が適用されず、評価回数を最小限にとどめることができる
    • 実際の振る舞いはちょっと微妙 (後述)

Lazy Evaluation

// 操作メソッドの前に asLazy() を挟むと
// LazyIterable に変換されて遅延評価になる 
Lists.mutable.of("a", "bb", "ccc", "dd", "e")
            .asLazy()  // <= ここに追加する
            .collect(s ->  s + s)
            .select(s -> s.length() > 5)
            .collect(s -> s.length())
            .getFirst(); // <= 最終的に1つだけあれば良いので 
                         //    select() で残る1つ目の要素までが
                         //    操作の適用対象となる

使ってみた所感

※以降の内容は個人的な云々...

便利ポイント

  • Factory クラスが大変使いやすい
    • 全てのコンテナで共通の構造をとっておりわかりやすい
    • どのコンテナに対しても簡単に初期化ができるので適当に機能を試すのが楽

便利ポイント

  • 割と便利なメソッドやクラス: detect, partition
    • detect() は毎度 select(...).getFirst() を省略できる
    • partition() は条件関数を受けて PartitionIterable<T> を返すメソッド
    • PartitionIterable は条件に対する selected / rejected だった要素それぞれのコンテナを持っていて、 getSelected() / getRejected() でそれぞれを取得できる

便利ポイント

  • (続き) PartitionIterable (partition()) を使う動機
    • select() / reject() の結果を一気に出して両方使いたい
    • でも forEach() の中でif文使ってどーのこーのするのは嫌
    • 欲を言うと collectSelected/Rejected とか取り出す前に処理を重ねられる Factory かAPI が欲しい、かも
      
Lists.mutable.of(1,2,3,4,5,6)
    .partition(n -> n % 2 == 0)
    .collectSelected(n -> doSomethingForSelected(n))
    .collectRejected(n -> doSomethingForRejected(n)) // みたいな
//  .separate() とかここにあるような Factory を経由して結果を取得するのも良い

// ただこれをやり始めるときりがなくなるしAPIデザインとして微妙なところ 

便利ポイント

  • With メソッドパターン
    • 適当な値を1つ関数の引数に足すことができる
    • メソッド参照を使用できる幅が広がった
      • メソッド参照が読みやすいかはまた別の問題
boolean anyPeopleHaveCats =
    people.anySatisfy(person -> person.hasPet(PetType.CAT));
// 上の Lambda 式のが下のようになる
boolean anyPeopleHaveCats =
    people.anySatisfyWith(Person::hasPet, PetType.CAT);
// 上のコードを Lambda に直すと
boolean anyPeopleHaveCats =
    people.anySatisfyWith((person, pet) -> person.hasPet(pet),
                          PetType.CAT);

困りポイント

  • Stream APIの命名に馴染みがあったのでメソッド名で混乱する
    • map/reduce/filter vs collect/injectInto/select
    • Stream APIにもcollect ってあって違う動きするから余計に...

    • これに関しては由来に理由がある
      • http://magazine.rubyist.net/?0038-MapAndCollect
      • map/reduce は Lisp 由来、 collect/inject は Smalltalk 由来
      • RichIterable の JavaDoc にも "RichIterable ... extends ... with several internal iterator methods, from the Smalltalk Collection protocol." って書いてある

困りポイント

  • Stream APIの命名に馴染みがあったのでメソッド名で混乱する (続き)
    • 慣れて
    • 8.0 から injectInto の Optional 対応みたいな感じで RichIterable に reduce というメソッドが追加された
      • ​他が detectOptional みたいな感じの中それでいいのか感

困りポイント

  • Immutable の面々
    • 読み取り以外にできることがない
      • 結合すらできないのは厳しい...
      • よく見たら Mutable も無かった
      • of() で要素としてコンテナを突っ込んで flatCollect() すればできるけどそういうことではない
    • 他のライブラリ/フレームワークと組合わせる時はキャストが必要
      • それは Stream API と使い心地が変わらないなあという気持ち
      • 途中から採用して Immutable だけ使うというのは難しいかも
      • 弊社はその辺のコストをとった感じ

困りポイント

  • 遅延処理化の使い勝手
    • ​asLazy() の返り値は LazyIterable<T> というものになり、若干使えるメソッドが減る
      • ​コンテナ特有の処理が不可能
      • Map 系はコンテナの形状が保てない
    • メソッドの評価回数が逐次処理の時より多くなるかもしれない​​

Eclipse Collections まとめ

  • 使い勝手の良い多機能コレクションライブラリ
  • Immutable 周りとか LazyIterable 改善されてほしい...
  • 身近にコミッタの方もいらっしゃるし気になることがあったらシュッとコントリビュートしたらいいのでは?
    • やっていくぞ 💪

質問タイム (あれば)

おしまい

jjug_ccc_2017_fall

By Shota Nagahori

jjug_ccc_2017_fall

Eclipse Collections の紹介

  • 5,145