From c0fda15c3b47494d54c7488d8679a84cf9cdaf5a Mon Sep 17 00:00:00 2001 From: Sergei Date: Tue, 13 Jul 2021 23:22:09 -0700 Subject: [PATCH] Adding Traversable.tapEach(Consumer) --- .../io/vavr/collection/AbstractMultimap.java | 5 ++ src/main/java/io/vavr/collection/Array.java | 10 +++ src/main/java/io/vavr/collection/BitSet.java | 5 ++ src/main/java/io/vavr/collection/CharSeq.java | 9 +++ .../java/io/vavr/collection/Collections.java | 8 +++ src/main/java/io/vavr/collection/HashMap.java | 5 ++ src/main/java/io/vavr/collection/HashSet.java | 5 ++ .../java/io/vavr/collection/Iterator.java | 5 ++ .../java/io/vavr/collection/LinearSeq.java | 3 + .../io/vavr/collection/LinkedHashMap.java | 5 ++ .../io/vavr/collection/LinkedHashSet.java | 5 ++ src/main/java/io/vavr/collection/List.java | 5 ++ .../io/vavr/collection/PriorityQueue.java | 9 +-- src/main/java/io/vavr/collection/Queue.java | 5 ++ src/main/java/io/vavr/collection/Seq.java | 3 + src/main/java/io/vavr/collection/Set.java | 3 + src/main/java/io/vavr/collection/Stream.java | 5 ++ .../java/io/vavr/collection/Traversable.java | 3 + src/main/java/io/vavr/collection/Tree.java | 5 ++ src/main/java/io/vavr/collection/TreeMap.java | 5 ++ src/main/java/io/vavr/collection/TreeSet.java | 5 ++ src/main/java/io/vavr/collection/Vector.java | 5 ++ .../collection/AbstractTraversableTest.java | 66 +++++++++++++++++++ src/test/java/io/vavr/collection/IntMap.java | 5 ++ .../java/io/vavr/collection/IntMultimap.java | 5 ++ 25 files changed, 190 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/vavr/collection/AbstractMultimap.java b/src/main/java/io/vavr/collection/AbstractMultimap.java index 67f63cbf4..a764e6389 100644 --- a/src/main/java/io/vavr/collection/AbstractMultimap.java +++ b/src/main/java/io/vavr/collection/AbstractMultimap.java @@ -441,6 +441,11 @@ public M peek(Consumer> action) { return (M) this; } + @Override + public M tapEach(Consumer> action) { + return peek(action); + } + @SuppressWarnings("unchecked") @Override public M replace(Tuple2 currentElement, Tuple2 newElement) { diff --git a/src/main/java/io/vavr/collection/Array.java b/src/main/java/io/vavr/collection/Array.java index 76d59b19d..2da85834b 100644 --- a/src/main/java/io/vavr/collection/Array.java +++ b/src/main/java/io/vavr/collection/Array.java @@ -1069,6 +1069,16 @@ public Array peek(Consumer action) { return this; } + @Override + public Array tapEach(Consumer action) { + Objects.requireNonNull(action, "action is null"); + for (int i = 0; i < length(); i++) { + final T value = get(i); + action.accept(value); + } + return this; + } + @Override public Array> permutations() { if (isEmpty()) { diff --git a/src/main/java/io/vavr/collection/BitSet.java b/src/main/java/io/vavr/collection/BitSet.java index 14987311d..f763938fb 100644 --- a/src/main/java/io/vavr/collection/BitSet.java +++ b/src/main/java/io/vavr/collection/BitSet.java @@ -475,6 +475,11 @@ public final BitSet peek(Consumer action) { return this; } + @Override + public final BitSet tapEach(Consumer action) { + return Collections.tapEach(this, action); + } + @Override public final String stringPrefix() { return "BitSet"; diff --git a/src/main/java/io/vavr/collection/CharSeq.java b/src/main/java/io/vavr/collection/CharSeq.java index fe49c11cb..6154974f5 100644 --- a/src/main/java/io/vavr/collection/CharSeq.java +++ b/src/main/java/io/vavr/collection/CharSeq.java @@ -746,6 +746,15 @@ public CharSeq peek(Consumer action) { return this; } + @Override + public CharSeq tapEach(Consumer action) { + Objects.requireNonNull(action, "action is null"); + for (int i = 0; i < back.length(); i++) { + action.accept(get(i)); + } + return this; + } + @Override public IndexedSeq permutations() { if (isEmpty()) { diff --git a/src/main/java/io/vavr/collection/Collections.java b/src/main/java/io/vavr/collection/Collections.java index a58171ea9..7775916a9 100644 --- a/src/main/java/io/vavr/collection/Collections.java +++ b/src/main/java/io/vavr/collection/Collections.java @@ -589,4 +589,12 @@ Object[] toArray() { } } + @SuppressWarnings("unchecked") + static > C tapEach(C source, Consumer action) { + Objects.requireNonNull(action, "action must not be null"); + return (C) source.map(t -> { + action.accept(t); + return t; + }); + } } diff --git a/src/main/java/io/vavr/collection/HashMap.java b/src/main/java/io/vavr/collection/HashMap.java index e6197517a..d6d3da4c7 100644 --- a/src/main/java/io/vavr/collection/HashMap.java +++ b/src/main/java/io/vavr/collection/HashMap.java @@ -735,6 +735,11 @@ public HashMap peek(Consumer> action) { return Maps.peek(this, action); } + @Override + public HashMap tapEach(Consumer> action) { + return Collections.tapEach(this, action); + } + @Override public HashMap put(K key, U value, BiFunction merge) { return Maps.put(this, key, value, merge); diff --git a/src/main/java/io/vavr/collection/HashSet.java b/src/main/java/io/vavr/collection/HashSet.java index 6c8fd6a94..21f63098b 100644 --- a/src/main/java/io/vavr/collection/HashSet.java +++ b/src/main/java/io/vavr/collection/HashSet.java @@ -735,6 +735,11 @@ public HashSet peek(Consumer action) { return this; } + @Override + public HashSet tapEach(Consumer action) { + return Collections.tapEach(this, action); + } + @Override public HashSet remove(T element) { final HashArrayMappedTrie newTree = tree.remove(element); diff --git a/src/main/java/io/vavr/collection/Iterator.java b/src/main/java/io/vavr/collection/Iterator.java index 4034e310a..4989034eb 100644 --- a/src/main/java/io/vavr/collection/Iterator.java +++ b/src/main/java/io/vavr/collection/Iterator.java @@ -1747,6 +1747,11 @@ public T next() { } } + @Override + default Iterator tapEach(Consumer action) { + return peek(action); + } + @Override default T reduceLeft(BiFunction op) { Objects.requireNonNull(op, "op is null"); diff --git a/src/main/java/io/vavr/collection/LinearSeq.java b/src/main/java/io/vavr/collection/LinearSeq.java index f401998f3..83ddde809 100644 --- a/src/main/java/io/vavr/collection/LinearSeq.java +++ b/src/main/java/io/vavr/collection/LinearSeq.java @@ -214,6 +214,9 @@ default int lastIndexWhere(Predicate predicate, int end) { @Override LinearSeq peek(Consumer action); + @Override + LinearSeq tapEach(Consumer action); + @Override LinearSeq> permutations(); diff --git a/src/main/java/io/vavr/collection/LinkedHashMap.java b/src/main/java/io/vavr/collection/LinkedHashMap.java index c2d3a7961..0cab919ff 100644 --- a/src/main/java/io/vavr/collection/LinkedHashMap.java +++ b/src/main/java/io/vavr/collection/LinkedHashMap.java @@ -755,6 +755,11 @@ public LinkedHashMap peek(Consumer> action) { return Maps.peek(this, action); } + @Override + public LinkedHashMap tapEach(Consumer> action) { + return Collections.tapEach(this, action); + } + @Override public LinkedHashMap put(K key, U value, BiFunction merge) { return Maps.put(this, key, value, merge); diff --git a/src/main/java/io/vavr/collection/LinkedHashSet.java b/src/main/java/io/vavr/collection/LinkedHashSet.java index a77e0ea36..90cf61f7f 100644 --- a/src/main/java/io/vavr/collection/LinkedHashSet.java +++ b/src/main/java/io/vavr/collection/LinkedHashSet.java @@ -756,6 +756,11 @@ public LinkedHashSet peek(Consumer action) { return this; } + @Override + public LinkedHashSet tapEach(Consumer action) { + return Collections.tapEach(this, action); + } + @Override public LinkedHashSet remove(T element) { final LinkedHashMap newMap = map.remove(element); diff --git a/src/main/java/io/vavr/collection/List.java b/src/main/java/io/vavr/collection/List.java index 23ce0bbcd..cb3c31101 100644 --- a/src/main/java/io/vavr/collection/List.java +++ b/src/main/java/io/vavr/collection/List.java @@ -1116,6 +1116,11 @@ public final List peek(Consumer action) { return this; } + @Override + public List tapEach(Consumer action) { + return Collections.tapEach(this, action); + } + @Override public final List> permutations() { if (isEmpty()) { diff --git a/src/main/java/io/vavr/collection/PriorityQueue.java b/src/main/java/io/vavr/collection/PriorityQueue.java index c1e46bfa8..7e12eae7f 100644 --- a/src/main/java/io/vavr/collection/PriorityQueue.java +++ b/src/main/java/io/vavr/collection/PriorityQueue.java @@ -28,10 +28,7 @@ import java.util.Comparator; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; +import java.util.function.*; import java.util.stream.Collector; /** @@ -663,6 +660,10 @@ public Comparator comparator() { return (Comparator) comparator; } + @Override + public PriorityQueue tapEach(Consumer action) { + return Collections.tapEach(this, action); + } /** * fun deleteMin [] = raise EMPTY diff --git a/src/main/java/io/vavr/collection/Queue.java b/src/main/java/io/vavr/collection/Queue.java index aa1900d72..6576e9138 100644 --- a/src/main/java/io/vavr/collection/Queue.java +++ b/src/main/java/io/vavr/collection/Queue.java @@ -1277,6 +1277,11 @@ public Queue takeRightWhile(Predicate predicate) { return takeRightUntil(predicate.negate()); } + @Override + public Queue tapEach(Consumer action) { + return Collections.tapEach(this, action); + } + /** * Transforms this {@code Queue}. * diff --git a/src/main/java/io/vavr/collection/Seq.java b/src/main/java/io/vavr/collection/Seq.java index d47b250ff..0418b6943 100644 --- a/src/main/java/io/vavr/collection/Seq.java +++ b/src/main/java/io/vavr/collection/Seq.java @@ -1224,6 +1224,9 @@ default U foldRight(U zero, BiFunction f) @Override Seq peek(Consumer action); + @Override + Seq tapEach(Consumer action); + @Override Seq replace(T currentElement, T newElement); diff --git a/src/main/java/io/vavr/collection/Set.java b/src/main/java/io/vavr/collection/Set.java index 58663873b..6b2687881 100644 --- a/src/main/java/io/vavr/collection/Set.java +++ b/src/main/java/io/vavr/collection/Set.java @@ -243,6 +243,9 @@ default boolean isDistinct() { @Override Set peek(Consumer action); + @Override + Set tapEach(Consumer action); + @Override Set replace(T currentElement, T newElement); diff --git a/src/main/java/io/vavr/collection/Stream.java b/src/main/java/io/vavr/collection/Stream.java index 87bf382e8..1b123fcde 100644 --- a/src/main/java/io/vavr/collection/Stream.java +++ b/src/main/java/io/vavr/collection/Stream.java @@ -1293,6 +1293,11 @@ public final Stream peek(Consumer action) { } } + @Override + public final Stream tapEach(Consumer action) { + return Collections.tapEach(this, action); + } + @Override public final Stream> permutations() { if (isEmpty()) { diff --git a/src/main/java/io/vavr/collection/Traversable.java b/src/main/java/io/vavr/collection/Traversable.java index c8e20576f..02259960a 100644 --- a/src/main/java/io/vavr/collection/Traversable.java +++ b/src/main/java/io/vavr/collection/Traversable.java @@ -1154,6 +1154,9 @@ default boolean nonEmpty() { @Override Traversable peek(Consumer action); + + Traversable tapEach(Consumer action); + /** * Calculates the product of this elements. Supported component types are {@code Byte}, {@code Double}, {@code Float}, * {@code Integer}, {@code Long}, {@code Short}, {@code BigInteger} and {@code BigDecimal}. diff --git a/src/main/java/io/vavr/collection/Tree.java b/src/main/java/io/vavr/collection/Tree.java index d231e757d..ab132ef7e 100644 --- a/src/main/java/io/vavr/collection/Tree.java +++ b/src/main/java/io/vavr/collection/Tree.java @@ -651,6 +651,11 @@ public final Tree peek(Consumer action) { return this; } + @Override + public final Tree tapEach(Consumer action) { + return Collections.tapEach(this, action); + } + @Override public final Tree replace(T currentElement, T newElement) { if (isEmpty()) { diff --git a/src/main/java/io/vavr/collection/TreeMap.java b/src/main/java/io/vavr/collection/TreeMap.java index bb60698e6..93709cc5f 100644 --- a/src/main/java/io/vavr/collection/TreeMap.java +++ b/src/main/java/io/vavr/collection/TreeMap.java @@ -1201,6 +1201,11 @@ public TreeMap peek(Consumer> action) { return Maps.peek(this, action); } + @Override + public TreeMap tapEach(Consumer> action) { + return Collections.tapEach(this, action); + } + @Override public TreeMap put(K key, U value, BiFunction merge) { return Maps.put(this, key, value, merge); diff --git a/src/main/java/io/vavr/collection/TreeSet.java b/src/main/java/io/vavr/collection/TreeSet.java index 98a479c10..784469cd3 100644 --- a/src/main/java/io/vavr/collection/TreeSet.java +++ b/src/main/java/io/vavr/collection/TreeSet.java @@ -805,6 +805,11 @@ public TreeSet peek(Consumer action) { return this; } + @Override + public TreeSet tapEach(Consumer action) { + return Collections.tapEach(this, action); + } + @Override public TreeSet remove(T element) { return new TreeSet<>(tree.delete(element)); diff --git a/src/main/java/io/vavr/collection/Vector.java b/src/main/java/io/vavr/collection/Vector.java index 2f882a008..ac54cb062 100644 --- a/src/main/java/io/vavr/collection/Vector.java +++ b/src/main/java/io/vavr/collection/Vector.java @@ -899,6 +899,11 @@ public Vector peek(Consumer action) { } return this; } + + @Override + public Vector tapEach(Consumer action) { + return Collections.tapEach(this, action); + } @Override public Vector> permutations() { diff --git a/src/test/java/io/vavr/collection/AbstractTraversableTest.java b/src/test/java/io/vavr/collection/AbstractTraversableTest.java index 99cd73c9c..14aec067e 100644 --- a/src/test/java/io/vavr/collection/AbstractTraversableTest.java +++ b/src/test/java/io/vavr/collection/AbstractTraversableTest.java @@ -42,6 +42,10 @@ public abstract class AbstractTraversableTest extends AbstractValueTest { + protected final boolean isLazy() { + return empty().isLazy(); + } + protected final boolean isTraversableAgain() { return empty().isTraversableAgain(); } @@ -50,6 +54,10 @@ protected final boolean isOrdered() { return empty().isOrdered(); } + protected final boolean isSequential() { + return empty().isSequential(); + } + protected abstract Collector, ? extends Traversable> collector(); @Override @@ -2844,6 +2852,64 @@ public void ofAllShouldReturnTheSingletonEmpty() { assertThat(ofAll(io.vavr.collection.Iterator.empty())).isSameAs(empty()); } + // eager traversable shall apply action immediately + + @Test + public void tapEachShouldThrowIfActionIsNull() { + assertThatThrownBy(() -> of("one", "two", "three").tapEach(null)) + .isInstanceOf(NullPointerException.class); + } + @Test + public void tapEachShouldDoNothingOnLazyTraversableUntilElementsMaterialized() { + // test is meaningful for lazy Traversable only + if (isLazy()) { + String errorMessage = "tapEach() must not call effect on lazy Traversable before its elements are materialized"; + of("one", "two", "three").tapEach(__ -> fail(errorMessage)); // note: no elements materialized yet + } + } + + @Test + public void tapEachShouldDoNothingOnEmptyTraversable() { + empty() + .tapEach(__ -> fail("tapEach() must never call effect on empty Traversable")) + .forEach(__ -> {}); // enforcing all elements to materialize + } + + @Test + public void tapEachShouldApplyEffectToEveryElementExactlyOnce() { + String[] values = {"one", "two", "three"}; + // some collections may not guarantee the order of effect applications, + // so here we'll assert the 'exactly once' property only + java.util.Map effectCallCounter = new java.util.HashMap<>(); + + // instantiating a Traversable from elements provided, tappping each element + // and forcing all of its elements to materialize + of(values) + .tapEach(element -> effectCallCounter.compute(element, (__, v) -> v == null ? 1 : v + 1)) + .forEach(__ -> {}); + + assertThat(effectCallCounter.get("one")).isEqualTo(1); // effect called for value "one" exactly once + assertThat(effectCallCounter.get("two")).isEqualTo(1); // ... for value "two" exactly once + assertThat(effectCallCounter.get("three")).isEqualTo(1); // ... for value "three" exactly once + assertThat(effectCallCounter.size()).isEqualTo(values.length); + } + + @Test + public void tapEachShouldApplyEffectToEveryElementInOrder() { + // testing effects order is for ordered Traversable only + if (this.isSequential()) { + String[] values = {"one", "two", "three"}; + java.util.List effectCallLog = new java.util.ArrayList<>(); + // instantiating a Traversable from elements provided, tappping each element + // and forcing all of its elements to materialize + of(values) + .tapEach(element -> effectCallLog.add(element)) + .forEach(__ -> {}); + + assertThat(effectCallLog).isEqualTo(Arrays.asList(values)); + } + } + private void testCollector(Runnable test) { if (isTraversableAgain()) { test.run(); diff --git a/src/test/java/io/vavr/collection/IntMap.java b/src/test/java/io/vavr/collection/IntMap.java index c8537630d..3481c67bf 100644 --- a/src/test/java/io/vavr/collection/IntMap.java +++ b/src/test/java/io/vavr/collection/IntMap.java @@ -247,6 +247,11 @@ public IntMap peek(Consumer action) { return this; } + @Override + public IntMap tapEach(Consumer action) { + return Collections.tapEach(this, action); + } + @Override public IntMap replace(T currentElement, T newElement) { final Option> currentEntryOpt = original.find(e -> e._2.equals(currentElement)); diff --git a/src/test/java/io/vavr/collection/IntMultimap.java b/src/test/java/io/vavr/collection/IntMultimap.java index 99ee05cb7..17477a2b2 100644 --- a/src/test/java/io/vavr/collection/IntMultimap.java +++ b/src/test/java/io/vavr/collection/IntMultimap.java @@ -248,6 +248,11 @@ public IntMultimap peek(Consumer action) { return this; } + @Override + public IntMultimap tapEach(Consumer action) { + return Collections.tapEach(this, action); + } + @Override public IntMultimap replace(T currentElement, T newElement) { final Option> currentEntryOpt = original.find(e -> e._2.equals(currentElement));