Make optional setter covariant#9
Conversation
|
Oh, I need to handle primitives specially here. Will fix later |
| ByteBuffer.class) | ||
| .addStatement("this.$1N.rewind()", spec.name) | ||
| .build(); | ||
| } else if (type.accept(TypeVisitor.IS_OPTIONAL)) { |
There was a problem hiding this comment.
Why is this change required?
There was a problem hiding this comment.
You can't assign a covariant type to an invariant type, like Optional<? extends Object> bar = getBar(); Optional<Object> foo = bar;. You need to either explicitly recast and suppress compiler warnings, or transform the generic type with Optional.<Object>of(bar.get())
(actually, I checked and it looks like Optional<Object> foo = (Optional<Object>) bar; works without java warnings so maybe that's better)
(oh, actually, it does produce a warning but it seems to be suppressed in generated code by default)
There was a problem hiding this comment.
I see. And you didn't want to widen the type everywhere?
There was a problem hiding this comment.
We could widen the type for optionals everywhere, but that feels a bigger / more scary change to me. If maintainers think that's a better approach, I'm happy to follow suit.
Intuitively I feel like it might require changes in many places; if ConjureOptional.getItem() starts returning Optional<? extends FOO> then a lot of code that currently expects Optional<FOO> will break; and I don't see an advantage to getting a wildcard type when reading
There was a problem hiding this comment.
I don't think there's much value in returning Optional<? extends FOO>, means you now have to propagate ? everywhere, and you can't assign it to a variable of type Optional<FOO> without a cast.
The value is there only when it's a parameter, since the compiler knows that FOO or e.g. BAR <: FOO both satisfy ? extends FOO.
| CodeBlock nullCheckedValue = Expressions.requireNonNull( | ||
| spec.name, enriched.fieldName().get() + " cannot be null"); | ||
| return CodeBlock.builder() | ||
| .addStatement("this.$1N = $2L.flatMap($3T::<$4T>::of)", |
There was a problem hiding this comment.
just filling in the syntax to make it clearer:
foo.flatMap(Optional::<Object>::of)doesn't seem valid to me/locally it does not compile for me.
There was a problem hiding this comment.
there needs to be some testing around both the code gen and the logic of the codegen in this PR
There was a problem hiding this comment.
Yeah, the extra :: was a typo that I already had fixed locally but forgot to push. I added a minor test but currently the infra for adding new cases is missing; will come back to it after #10 is fixed
|
what's a concrete use case for this? typically, our conjure objects have concrete types and there's no class hierarchy. |
|
@uschi2000 the biggest benefit is for With current bindings, you get this gotcha: With wildcard builder you get |
| String value = "foo"; | ||
| Optional<String> maybeValue = Optional.of(value); | ||
|
|
||
| // FIXME: this doesn't compile right now because changes to example-types.yml aren't picked up |
There was a problem hiding this comment.
there's a test here that re-generates the code when you comment out a if (REGEN) - ish statement
There was a problem hiding this comment.
I believe that regenerates the java code based on the .json definitions, but there's nothing that regenerates the .json definitions right now, see #10
|
Green! |
|
Given the use-case, I wonder if we only want to be covariant for |
|
I think it's also possibly handy for things like collections (being able to put an |
Even though most of the items we take are final, it's better to be covariant on everything
than not be covariant on anything - in particular covariance for collections of Objects
makes for a more natural api, where `Optional<String> foo = Optional.of("foo"); return AnyOptional.of(foo);`
results in `Optional.of("foo")` rather than `Optional.of(Optional.of("foo"))`;
We could in theory track if the class being contained is final (a conjure object / a primitive), but
it doesn't seem worth the effort; taking `Optional<? extends String>` seems OK even if nothing can
extend string.
|
Another related annoyance is the static builders for collections: but at a quick glance it doesn't seem trivial to widen the parameters there (the |
| @JsonSetter("item") | ||
| public Builder item(Optional<String> item) { | ||
| this.item = Objects.requireNonNull(item, "item cannot be null"); | ||
| this.item = (Optional<String>) Objects.requireNonNull(item, "item cannot be null"); |
|
|
||
| @JsonSetter("doubleItems") | ||
| public Builder doubleItems(Iterable<Double> doubleItems) { | ||
| public Builder doubleItems(Iterable<? extends Double> doubleItems) { |
There was a problem hiding this comment.
As a rule of thumb, we've tried to make conjure-java generate code that is as close as possible to what a human might write. Can we avoid the ? extends for known classes like String, Double, Integer etc?
|
|
||
| @JsonSetter("errors") | ||
| public Builder errors(Iterable<ErrorDefinition> errors) { | ||
| public Builder errors(Iterable<? extends ErrorDefinition> errors) { |
There was a problem hiding this comment.
I'm also not a fan of this. When a builder accepts a conjure type, we know it is final so there's no point adding ? extends
|
@JacekLach Dan and I had a couple of CR comments above - what do you think? |
|
I agree with the sentiment behind them, just didn't have time to look into how to make it happen. The answer is probably to inspect if the type is final somehow. |
|
@iamdanfox I think this does it. I'll let the |
Non-any primitives, conjure references, and optionals are always final, and so there's no point in widening functions that take iterables of those.
| } | ||
|
|
||
| public Builder addAllArgs(Iterable<ArgumentDefinition> args) { | ||
| public Builder addAllArgs(Iterable<? extends ArgumentDefinition> args) { |
There was a problem hiding this comment.
I think these shouldn't have changed?
iamdanfox
left a comment
There was a problem hiding this comment.
I think this makes sense to me as it prevents the nasty footgun you encountered (putting Optional.of(x) into a builder method, resulting in an unwanted double-nested optional).
The code necessary to achieve this does seem pretty hard to read, but I think the outcome is probably worth it.
* Add ConjureGeneratorParameters as a map builder * don't allow nulls * ConjureGeneratorParametersRenderer * stop throwing groovy exceptions * make it static * Rewrite ConjureExtension to use ConjureGeneratorParameters for typescript and java * Allow false to fall through as --flag=false * rename to generatorParameters * enforce camelCase key names * CompileConjureJavaTask - capture out/err, log args at info * Use rewritten feature flag retrofitCompletableFutures * Rename ConjureGeneratorParameters -> GeneratorOptions * GeneratorOptions copy constructor * Fixup conjure typescript compilation according to Felipe's changes * Bump conjure-java to 0.2.4 * generatorOptionsSupplier -> options * lolz * go away errorprone, tis not a service * disable Slf4jLogsafeArgs cos we are not a service / consumed by a service * no dist * conjure-typescript 0.5.0 * unnnecessary * one line * log info in typescript too * log if override is same as defaultˆ * Move GeneratorOptions, ConjureExtension to a gradle-conjure-api * Move GeneratorOptions, ConjureExtension to a gradle-conjure-api * Use additionalArgs * Move default parameter handling t oRenderGeneratorOptions * oops * fix bug again, add tests * getTypescript / getJava * And get rid of copy constructor now that we don't need it anymore * simplify * only support defaults * no conjure core here * don't depend on conjure-core, just guava
Before this PR
After this PR