Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Non-boxing 'addAll' Methods for Primitive Types #2389

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

bmarcaur
Copy link
Member

@bmarcaur bmarcaur commented Oct 22, 2024

Before this PR

This is an alternative variant of #2312 which attempts to fix the boxing overhead merely introduced by the shape of our API (primitive types on both sides of the abstraction). However, there was some objection to that approach due to some stylistic and API design disagreements. This alternative seeks to remedy some of those concerns by following some "prior art" from libraries we accept / use all the time - Immutables.

Immutables will generate a vararg addAll variant for a List collection of some primitive boxed type. For example, List<Long> will produce something like:

public final Builder addField(double... elements)

After this PR

This change first punches a hole through from the underlying eclipse-collections types e.g. DoubleList which has routines like public boolean addAll(int... source) which allow for direct, non-boxed, additions to the type.

Then, taking Immutables as inspiration, I wired up our codegen to leverage this abstraction. This alternative design addresses the mutability concerns as this API clearly communicates how data will be owned moving forward (i.e. copied). This implementation also addresses the concerns of having a setter that resets the type may cause order of operations bugs in the field while populating the builder.

Of Note

Yes... I am aware that the Iterable variant is a setter in the builder e.g.:

@JsonSetter(value = "primitiveItems", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL)
public Builder primitiveItems(@Nonnull Iterable<Integer> primitiveItems) {
        checkNotBuilt();
        this.primitiveItems = ConjureCollections.newNonNullIntegerList(
                Preconditions.checkNotNull(primitiveItems, "primitiveItems cannot be null"));
        return this;
}

This feels like a design oversight (IMO) seeing as how primitiveItems is already initialized statically and newNonNullIntegerList simply performs a copy using addAll. Removing it from the field will be considerable work, so this will need to be inconsistent for the time being. And also previous design deficiencies cannot justify future designs.

Tangent

Sorry if this is getting off topic, but that Iterable variant seems destined to be used incorrectly. Imagine the following code:

// Has a field List<String> items
SomeConjureObject.Builder builder = SomeConjureObject.builder();
String firstToAdd = "first";
List<String> someOtherStrings = List.of(...);

// Clearly an adder
builder.items(firstToAdd);
// Adder? Setter? Setter! No longer contains firstToAdd
builder.items(someOtherStrings);

// Or how about this
// Imagine your code starts off like this
String firstToAdd = "first";
String secondToAdd = someMethod();
builder.items(firstToAdd);
builder.items(secondToAdd);

// But later the API of someMethod changes and now returns a List<String>
// the type checker is happy but now (below) is a setter and no longer
// contains firstToAdd
builder.items(secondToAdd);

Either way, necessary or not, I do not love the invalid state that is further representable on this API by adding an additional similarly named setter.

@@ -218,6 +218,13 @@ public Builder primitiveItems(@Nonnull Iterable<Integer> primitiveItems) {
return this;
}

public Builder addAllPrimitiveItems(@Nonnull int... primitiveItems) {
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... if my words in the PR description are to be believed and I am mirroring Immutables, then this method should be generated with a name primitiveItems. Very easy change to make if people find that clearer.

@@ -56,6 +56,14 @@ public boolean addAll(int index, Collection<? extends SafeLong> collection) {
return delegate.addAllAtIndex(index, target);
}

public void addAll(long... source) {
for (long value : source) {
// Doesn't use SafeLong creation because this causes unnecessary boxing
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 is a little too specific and doesn't explain how the underlying type holds longs and those must adhere to the abstraction.

*/
// This method modifies a list that can't handle nulls. Do not use this unless the nonNullCollections flag is set
public static void addAllToDoubleList(Collection<Double> addTo, double[] elementsToAdd) {
if (addTo instanceof ConjureDoubleList) {
Copy link
Member Author

Choose a reason for hiding this comment

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

An inelegant solution to preserve the feedback on #2307 (comment), but still capture the optimization we added. Also has some prior art in addAll.

@@ -40,7 +40,7 @@ public long longValue() {
return longValue;
}

private static long check(long value) {
public static long check(long value) {
Copy link
Member Author

Choose a reason for hiding this comment

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

I increased the visibility on this, because the logic is simple enough that to satisfy it is tantamount to copying the implementation detail (which felt bad). But also the logic is so simple that we aren't overly exposing some internals. Besides having more places rely on this is likely to prevent skew between implementations (IMO).

LIST("List", false),
DOUBLE_LIST("DoubleList", true),
INTEGER_LIST("IntegerList", true),
BOOLEAN_LIST("BooleanList", true),
Copy link
Member Author

Choose a reason for hiding this comment

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

But also maybe not? #2390

@@ -66,7 +100,7 @@ public static MethodSpec.Builder createCollectionSetterBuilder(
field.name));
}

public static MethodSpec.Builder createOptionalSetterBuilder(
Copy link
Member Author

Choose a reason for hiding this comment

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

I can extract this into a separate PR, but I noticed that we were overly exposing these methods which in turn was overly exposing the EnrichedField type.

@bmarcaur bmarcaur marked this pull request as ready for review October 23, 2024 15:24
@bmarcaur bmarcaur force-pushed the bmarcaurele/primitive-add-all branch from df23c89 to 5590778 Compare November 9, 2024 23:47
@@ -767,10 +787,19 @@ private List<MethodSpec> createAuxiliarySetters(EnrichedField enriched, boolean
Type type = enriched.conjureDef().getType();
Optional<LogSafety> safety = safetyEvaluator.getUsageTimeSafety(enriched.conjureDef());

ImmutableList.Builder<MethodSpec> builder = ImmutableList.builder();
Copy link
Member Author

Choose a reason for hiding this comment

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

Made this a builder so I could optionally add another method and not have to duplicate logic to return lists directly.

@bmarcaur bmarcaur force-pushed the bmarcaurele/primitive-add-all branch from 5590778 to b021276 Compare November 25, 2024 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant