Skip to content

Make optional setter covariant#9

Merged
iamdanfox merged 7 commits into
palantir:developfrom
JacekLach:jl/optional-covariance
Sep 5, 2018
Merged

Make optional setter covariant#9
iamdanfox merged 7 commits into
palantir:developfrom
JacekLach:jl/optional-covariance

Conversation

@JacekLach
Copy link
Copy Markdown
Contributor

@JacekLach JacekLach commented May 30, 2018

Before this PR

// imagine this comes from a function and you don't know if it's present or absent
Optional<String> specificValue = Optional.of("foo");
RequestArgs args = RequestArgs.builder()
  // optionalArgument is `optional<any>` because different requests take different types
  .optionalArgument(specificValue)
  .build();
args.getOptionalArgument() // returns Optional.of(Optional.of("foo"))

After this PR

Optional<String> specificValue = Optional.of("foo");
RequestArgs args = RequestArgs.builder()
  // optionalArgument is `optional<any>` because different requests take different types
  .optionalArgument(specificValue)
  .build();
args.getOptionalArgument() // returns Optional.of("foo")

@JacekLach
Copy link
Copy Markdown
Contributor Author

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)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this change required?

Copy link
Copy Markdown
Contributor Author

@JacekLach JacekLach May 31, 2018

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see. And you didn't want to widen the type everywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

there needs to be some testing around both the code gen and the logic of the codegen in this PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

@uschi2000
Copy link
Copy Markdown
Contributor

what's a concrete use case for this? typically, our conjure objects have concrete types and there's no class hierarchy.

@JacekLach
Copy link
Copy Markdown
Contributor Author

JacekLach commented May 31, 2018

@uschi2000 the biggest benefit is for Optional<any>, which sees some usage.

With current bindings, you get this gotcha:

 // imagine this comes from a function and you don't know if it's present or absent
Optional<String> specificValue = Optional.of("foo");
RequestArgs args = RequestArgs.builder()
  // optionalArgument is `optional<any>` because different requests take different types
  .optionalArgument(specificValue)
  .build();
args.getOptionalArgument() // returns Optional.of(Optional.of("foo"))

With wildcard builder you get Optional.of("foo") in the last example, which I think you more often expect. Of course it means if you actually want a directly nested optional in an untyped field you need to put in some more boilerplate; but that seems a very rare occurrence.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

there's a test here that re-generates the code when you comment out a if (REGEN) - ish statement

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

@JacekLach
Copy link
Copy Markdown
Contributor Author

Green!

@markelliot
Copy link
Copy Markdown
Contributor

Given the use-case, I wonder if we only want to be covariant for any to limit abuse.

@JacekLach
Copy link
Copy Markdown
Contributor Author

JacekLach commented Jun 1, 2018

I think it's also possibly handy for things like collections (being able to put an Optional<SortedSet> into a optional<set<string>>), but it is only in the case of any that without this change you transparently use the wrong overload, instead of getting a compilation error. So I'm OK with just doing this for any at first, and extending if real use-case shows up.

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.
@JacekLach
Copy link
Copy Markdown
Contributor Author

Another related annoyance is the static builders for collections:

    public static CovariantListExample of(List<Object> items) {
        return builder().items(items).build();
    }

but at a quick glance it doesn't seem trivial to widen the parameters there (the BeanBuilderGenerator and BeanGenerator seem to operate differently)

@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");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this cast doesn't seem necessary

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yep, missed out on fixing tests after 5435159

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

530b826 should address this and fix tests


@JsonSetter("doubleItems")
public Builder doubleItems(Iterable<Double> doubleItems) {
public Builder doubleItems(Iterable<? extends Double> doubleItems) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@iamdanfox
Copy link
Copy Markdown
Contributor

@JacekLach Dan and I had a couple of CR comments above - what do you think?

@JacekLach
Copy link
Copy Markdown
Contributor Author

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.

@JacekLach
Copy link
Copy Markdown
Contributor Author

@iamdanfox I think this does it.

I'll let the .of constructors be for now even though they make me sad, since at least you can now use the builder when you need to :P

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think these shouldn't have changed?

Copy link
Copy Markdown
Contributor

@iamdanfox iamdanfox left a comment

Choose a reason for hiding this comment

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

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.

@iamdanfox iamdanfox merged commit 7a00cb7 into palantir:develop Sep 5, 2018
@iamdanfox iamdanfox deleted the jl/optional-covariance branch September 5, 2018 15:51
carterkozak pushed a commit to carterkozak/conjure-java that referenced this pull request Dec 24, 2018
* 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
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.

5 participants