-
Notifications
You must be signed in to change notification settings - Fork 43
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
[draft] Generate 'sealed interfaces' for Conjure unions (feature flagged) #1838
base: develop
Are you sure you want to change the base?
Conversation
defaultImpl = UnknownWrapper.class) | ||
@JsonSubTypes(@JsonSubTypes.Type(FooWrapper.class)) | ||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
public sealed interface SingleUnion { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few things I might suggest changing:
- We could use this opportunity to become more opinionated about the visitor. It's a lot of code that becomes unnecessary with sealed type pattern matching. What if we drop the visitor entirely?
- We're exposing the
*Wrapper
types as public api, we can rename them dropping theWrapper
suffix to make them more concise.SingleUnion.Foo
is pretty clear imo. - Possibly a controversial take: What if we drop the static factories, and use standard records to implement the union type? We don't use constructors in most places, however these are wrappers around a single value (or a single key-value-pair in the case of unknown).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've got some open threads about some of these questions on https://quip/3YEnAt26mmtE - to summarise:
what if we drop the visitor entirely?
- as a long-term state, I definitely agree we should ditch the visitor. My thinking is that it'll probably be most efficient to keep generating it for now, to make the adoption of this flag as easy as possible.
- I want to write the error-prone rule to rewrite visitors -> switch expressions. Then maybe when it's fully switched you over, it would flip another feature flag to stop generating these?
- I had the tantalising thought that maybe if you have sourceCompat=17, we could actually just start generating these new sealed interfaces for everyone (because sealed interfaces are GA in 17, it's only the pattern-matching-switch-expressions that require the
--enable-preview
thing)... sadly I think revapi/ABI changes might derail this plan though.
*Wrapper
types - 100% agree - this is just a commit I haven't made yet.Use standard records to implement the union type
- yep I'm planning on doing this too, especially because I think destructuring is coming which should make the switch statements even nicer https://openjdk.org/jeps/405.
drop the static factories
I think I actually wanna keep them, because I find the IntelliJ autocomplete very natural when you typeMyUnion.
and then you get a nice dropdown of all the variants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also intend to add that .Known
sub interface because i think the .throwOnUnknown
ergonomics will be quite neat
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re dropping the visitor: I suppose it becomes a sequencing problem -- my preference is generally to reach for the aspirational goal, which we can set in new template projects, and follow-up with the sanest possible migration path.
Re jep405: I thought this was why you were opting into experimental features on jdk17, I don't think the sealed types and such need the experimental pattern matching support otherwise, no?
I find the IntelliJ autocomplete very natural when you type
MyUnion.
Wouldn't that be equivalent to new MyUnion.
? If we're using records, I'd prefer not to provide two public constructors for the same type, as that forces developers to decide between the two.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main reason I have been opting-in to --enable-preview
is strictly to get https://openjdk.org/jeps/406 (pattern matching for switch). I only just discovered https://openjdk.org/jeps/405 (Record Patterns), but I think it'll be another usability benefit!
9edbb1f
to
c4adf00
Compare
c4adf00
to
5ead7ed
Compare
@JsonIgnoreProperties(ignoreUnknown = true) | ||
public sealed interface Union2 { | ||
|
||
sealed interface Known permits Foo, Bar, Baz {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sealed interface Known permits Foo, Bar, Baz {} | |
sealed interface Known extends Union2 permits Foo, Bar, Baz {} |
Then below we don't need to implement Union2, Known
, only Known
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea here is that we can use MyUnion.Known
internally for business logic, ensuring that nothing has to validate against unknown variants, but no casting/validation is required to return or pass MyUnion.Known
as a MyUnion
.
We may be able to do something clever with safety annotations on MyUnion.Known
that can't be guaranteed on the base type with an unknown variant (some issues today since we don't allow safety to be redefined as safer than a supertype, but I think it's reasonable in this instance, trick is ensuring we know when is safe, and when isn't)
@JsonTypeName("baz") | ||
record Baz(long value) implements Union2, Known { | ||
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES) | ||
public Baz(@JsonSetter("baz") @Nonnull long value) { | ||
Preconditions.checkNotNull(value, "baz cannot be null"); | ||
this.value = value; | ||
} | ||
|
||
@JsonProperty(value = "type", index = 0) | ||
@SuppressWarnings("UnusedMethod") | ||
private String getType() { | ||
return "baz"; | ||
} | ||
|
||
@JsonProperty("baz") | ||
public long getValue() { | ||
return value; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Entirely untested, but I wonder if can do something like this:
@JsonTypeName("baz") | |
record Baz(long value) implements Union2, Known { | |
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES) | |
public Baz(@JsonSetter("baz") @Nonnull long value) { | |
Preconditions.checkNotNull(value, "baz cannot be null"); | |
this.value = value; | |
} | |
@JsonProperty(value = "type", index = 0) | |
@SuppressWarnings("UnusedMethod") | |
private String getType() { | |
return "baz"; | |
} | |
@JsonProperty("baz") | |
public long getValue() { | |
return value; | |
} | |
} | |
@JsonTypeName("baz") | |
@JsonAppend(attrs = @JsonAppend.Attr(propName = "type", value = "baz"), prepend = true) | |
record Baz(long value) implements Union2, Known { | |
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES) | |
public Baz(@JsonSetter("baz") @Nonnull long value) { | |
Preconditions.checkNotNull(value, "baz cannot be null"); | |
this.value = value; | |
} | |
@JsonProperty("baz") | |
public long getValue() { | |
return value; | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Diff didn't render how I expected... The above suggests adding an annotation to the record: @JsonAppend(attrs = @JsonAppend.Attr(propName = "type", value = "baz"), prepend = true)
and removing the getType()
method.
Before this PR
For hackweek, I'm hoping to enable folks to use Java 17's pattern-matching-switch-expressions (https://openjdk.org/jeps/406) to visit the different possible variants of a Conjure union. My primary goal is to make code more readable, but I think it should come with a performance boost too.
After this PR
==COMMIT_MSG==
A new feature flag (
--sealedUnions
) allows users to opt-in to generating Java 17 sealed interfaces for their conjure unions.==COMMIT_MSG==
Other related work is tracked on https://quip/3YEnAt26mmtE.
Outstanding TODOs:
throwOnKnown
methodSafeIllegalArgumentException
now?Known
interface (maybe just optional initially?)--enable-preview
working in Intellij ([draft]com.palantir.baseline-enable-preview-flag
works with IntelliJ integration correctly gradle-baseline#2319) so I can write some testsPossible downsides?