Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 70 additions & 57 deletions src/main/java/graphql/util/FpKit.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
Expand All @@ -18,69 +19,90 @@
import java.util.OptionalInt;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Collections.singletonList;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.mapping;

@Internal
public class FpKit {

//
// From a list of named things, get a map of them by name, merging them according to the merge function
public static <T> Map<String, T> getByName(List<T> namedObjects, Function<T, String> nameFn, BinaryOperator<T> mergeFunc) {
return namedObjects.stream().collect(Collectors.toMap(
nameFn,
identity(),
mergeFunc,
LinkedHashMap::new)
);
return toMap(namedObjects, nameFn, mergeFunc);
}

//
// From a collection of keyed things, get a map of them by key, merging them according to the merge function
public static <T, NewKey> Map<NewKey, T> toMap(Collection<T> collection, Function<T, NewKey> keyFunction, BinaryOperator<T> mergeFunc) {
Map<NewKey, T> resultMap = new LinkedHashMap<>();
for (T obj : collection) {
NewKey key = keyFunction.apply(obj);
if (resultMap.containsKey(key)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it one less statement by looking up the value and save it one call

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically not the right semantics in a general sense - sure there could be non

If this map permits null values, then a return value of null does not necessarily indicate that the map contains no mapping for the key; it's also possible that the map explicitly maps the key to null. The containsKey operation may be used to distinguish these two cases.

T existingValue = resultMap.get(key);
T mergedValue = mergeFunc.apply(existingValue, obj);
resultMap.put(key, mergedValue);
} else {
resultMap.put(key, obj);
}
}
return resultMap;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new common generic function - used in other places

}

// normal groupingBy but with LinkedHashMap
public static <T, NewKey> Map<NewKey, ImmutableList<T>> groupingBy(Collection<T> list, Function<T, NewKey> function) {
return list.stream().collect(Collectors.groupingBy(function, LinkedHashMap::new, mapping(Function.identity(), ImmutableList.toImmutableList())));
return filterAndGroupingBy(list, ALWAYS_TRUE, function);
}

@SuppressWarnings("unchecked")
public static <T, NewKey> Map<NewKey, ImmutableList<T>> filterAndGroupingBy(Collection<T> list,
Predicate<? super T> predicate,
Function<T, NewKey> function) {
return list.stream().filter(predicate).collect(Collectors.groupingBy(function, LinkedHashMap::new, mapping(Function.identity(), ImmutableList.toImmutableList())));
}
//
// The cleanest version of this code would have two maps, one of immutable list builders and one
// of the built immutable lists. BUt we are trying to be performant and memory efficient so
// we treat it as a map of objects and cast like its Java 4x
//
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment above explains the whacky code. One less map allocation

Alternative is

public static <T, NewKey> Map<NewKey, ImmutableList<T>> groupingBy(Collection<T> list, Function<T, NewKey> function) {
        Map<NewKey, ImmutableList.Builder<T>> tempMap = new LinkedHashMap<>();
        for (T item : list) {
            NewKey key = function.apply(item);
            tempMap.computeIfAbsent(key, k -> ImmutableList.builder()).add(item);
        }
        Map<NewKey, ImmutableList<T>> resultMap = new LinkedHashMap<>();
        for (Map.Entry<NewKey, ImmutableList.Builder<T>> entry : tempMap.entrySet()) {
            resultMap.put(entry.getKey(), entry.getValue().build());
        }
        return resultMap;
    }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note two allocations - neater code - worse performance

Map<NewKey, Object> resutMap = new LinkedHashMap<>();
for (T item : list) {
if (predicate.test(item)) {
NewKey key = function.apply(item);
// we have to use an immutable list builder as we built it out
((ImmutableList.Builder<Object>) resutMap.computeIfAbsent(key, k -> ImmutableList.builder()))
.add(item);
}
}
if (resutMap.isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you meant to call it a resultMap, anyway not blocking this PR

return Collections.emptyMap();
}
// Convert builders to ImmutableLists in place to avoid an extra allocation
// yes the code is yuck - but its more performant yuck!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind if I borrow this line for the release notes?

resutMap.replaceAll((key, builder) ->
((ImmutableList.Builder<Object>) builder).build());

public static <T, NewKey> Map<NewKey, ImmutableList<T>> groupingBy(Stream<T> stream, Function<T, NewKey> function) {
return stream.collect(Collectors.groupingBy(function, LinkedHashMap::new, mapping(Function.identity(), ImmutableList.toImmutableList())));
// make it the right shape - like as if generics were never invented
return (Map<NewKey, ImmutableList<T>>) (Map<?, ?>) resutMap;
}

public static <T, NewKey> Map<NewKey, T> groupingByUniqueKey(Collection<T> list, Function<T, NewKey> keyFunction) {
return list.stream().collect(Collectors.toMap(
keyFunction,
identity(),
throwingMerger(),
LinkedHashMap::new)
);
public static <T, NewKey> Map<NewKey, T> toMapByUniqueKey(Collection<T> list, Function<T, NewKey> keyFunction) {
return toMap(list, keyFunction, throwingMerger());
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was renamed from groupingByUniqueKey to toMapByUniqueKey because its NOT a group by at all. Its a toMap - it was badly named


public static <T, NewKey> Map<NewKey, T> groupingByUniqueKey(Stream<T> stream, Function<T, NewKey> keyFunction) {
return stream.collect(Collectors.toMap(
keyFunction,
identity(),
throwingMerger(),
LinkedHashMap::new)
);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never used - so I removed it - one less stream thing to port


private static final Predicate<Object> ALWAYS_TRUE = o -> true;

private static final BinaryOperator<Object> THROWING_MERGER_SINGLETON = (u, v) -> {
throw new IllegalStateException(String.format("Duplicate key %s", u));
};


private static <T> BinaryOperator<T> throwingMerger() {
return (u, v) -> {
throw new IllegalStateException(String.format("Duplicate key %s", u));
};
//noinspection unchecked
return (BinaryOperator<T>) THROWING_MERGER_SINGLETON;
}


Expand Down Expand Up @@ -240,11 +262,6 @@ public static <T> List<T> valuesToList(Map<?, T> map) {
return new ArrayList<>(map.values());
}

public static <K, V, U> List<U> mapEntries(Map<K, V> map, BiFunction<K, V, U> function) {
return map.entrySet().stream().map(entry -> function.apply(entry.getKey(), entry.getValue())).collect(Collectors.toList());
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never used so I removed it - one less stream thing to port



public static <T> List<List<T>> transposeMatrix(List<? extends List<T>> matrix) {
int rowCount = matrix.size();
int colCount = matrix.get(0).size();
Expand All @@ -261,21 +278,13 @@ public static <T> List<List<T>> transposeMatrix(List<? extends List<T>> matrix)
return result;
}

public static <T> CompletableFuture<List<T>> flatList(CompletableFuture<List<List<T>>> cf) {
return cf.thenApply(FpKit::flatList);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never used - no need to port it


public static <T> List<T> flatList(Collection<List<T>> listLists) {
return listLists.stream()
.flatMap(List::stream)
.collect(ImmutableList.toImmutableList());
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never used - no need to port it


public static <T> Optional<T> findOne(Collection<T> list, Predicate<T> filter) {
return list
.stream()
.filter(filter)
.findFirst();
for (T t : list) {
if (filter.test(t)) {
return Optional.of(t);
}
}
return Optional.empty();
}

public static <T> T findOneOrNull(List<T> list, Predicate<T> filter) {
Expand All @@ -292,10 +301,13 @@ public static <T> int findIndex(List<T> list, Predicate<T> filter) {
}

public static <T> List<T> filterList(Collection<T> list, Predicate<T> filter) {
return list
.stream()
.filter(filter)
.collect(Collectors.toList());
List<T> result = new ArrayList<>();
for (T t : list) {
if (filter.test(t)) {
result.add(t);
}
}
return result;
}

public static <T> Set<T> filterSet(Collection<T> input, Predicate<T> filter) {
Expand Down Expand Up @@ -352,9 +364,10 @@ public static <T> Supplier<T> interThreadMemoize(Supplier<T> delegate) {
/**
* Faster set intersection.
*
* @param <T> for two
* @param <T> for two
* @param set1 first set
* @param set2 second set
*
* @return intersection set
*/
public static <T> Set<T> intersection(Set<T> set1, Set<T> set2) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/graphql/util/NodeMultiZipper.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public T toRootNode() {
Map<T, ImmutableList<NodeZipper<T>>> sameParent = zipperWithSameParent(deepestZippers);

List<NodeZipper<T>> newZippers = new ArrayList<>();
Map<T, NodeZipper<T>> zipperByNode = FpKit.groupingByUniqueKey(curZippers, NodeZipper::getCurNode);
Map<T, NodeZipper<T>> zipperByNode = FpKit.toMapByUniqueKey(curZippers, NodeZipper::getCurNode);
for (Map.Entry<T, ImmutableList<NodeZipper<T>>> entry : sameParent.entrySet()) {
NodeZipper<T> newZipper = moveUp(entry.getKey(), entry.getValue());
Optional<NodeZipper<T>> zipperToBeReplaced = Optional.ofNullable(zipperByNode.get(entry.getKey()));
Expand Down
96 changes: 95 additions & 1 deletion src/test/groovy/graphql/util/FpKitTest.groovy
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package graphql.util


import com.google.common.collect.ImmutableList
import spock.lang.Specification

import java.util.function.Supplier
Expand Down Expand Up @@ -98,6 +98,100 @@ class FpKitTest extends Specification {
l == ["Parrot"]
}

class Person {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey look - TESTS!

We never had very many tests for FpKit

So I I write them first - then changed the implementation

TDD!!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noice

String name
String city

Person(String name) {
this.name = name
}

Person(String name, String city) {
this.name = name
this.city = city
}

String getName() {
return name
}

String getCity() {
return city
}
}

def a = new Person("a", "New York")
def b = new Person("b", "New York")
def c1 = new Person("c", "Sydney")
def c2 = new Person("c", "London")

def "getByName tests"() {

when:
def map = FpKit.getByName([a, b, c1, c2], { it -> it.getName() })
then:
map == ["a": a, "b": b, c: c1]

when:
map = FpKit.getByName([a, b, c1, c2], { it -> it.getName() }, { it1, it2 -> it2 })
then:
map == ["a": a, "b": b, c: c2]
}

def "groupingBy tests"() {

when:
Map<String, ImmutableList<Person>> map = FpKit.groupingBy([a, b, c1, c2], { it -> it.getCity() })
then:
map == ["New York": [a, b], "Sydney": [c1], "London": [c2]]

when:
map = FpKit.filterAndGroupingBy([a, b, c1, c2], { it -> it != c1 }, { it -> it.getCity() })
then:
map == ["New York": [a, b], "London": [c2]]

}

def "toMapByUniqueKey works"() {

when:
Map<String, Person> map = FpKit.toMapByUniqueKey([a, b, c1], { it -> it.getName() })
then:
map == ["a": a, "b": b, "c": c1]

when:
FpKit.toMapByUniqueKey([a, b, c1, c2], { it -> it.getName() })
then:
def e = thrown(IllegalStateException.class)
e.message.contains("Duplicate key")
}

def "findOne test"() {
when:
def opt = FpKit.findOne([a, b, c1, c2], { it -> it.getName() == "c" })
then:
opt.isPresent()
opt.get() == c1

when:
opt = FpKit.findOne([a, b, c1, c2], { it -> it.getName() == "d" })
then:
opt.isEmpty()

when:
opt = FpKit.findOne([a, b, c1, c2], { it -> it.getName() == "a" })
then:
opt.isPresent()
opt.get() == a
}

def "filterList works"() {
when:
def list = FpKit.filterList([a, b, c1, c2], { it -> it.getName() == "c" })
then:
list == [c1, c2]
}

def "set intersection works"() {
def set1 = ["A","B","C"] as Set
def set2 = ["A","C","D"] as Set
Expand Down
Loading