-
Notifications
You must be signed in to change notification settings - Fork 25.4k
Helper for ConstructingObjectParser for record classes #132106
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
base: main
Are you sure you want to change the base?
Conversation
Pinging @elastic/es-core-infra (Team:Core/Infra) |
This seemed helpful, but it probably causes more harm than good by causing confusing errors when publicLookup doesn't have the required access. Better to make the caller pass a Lookup object that has the required access.
MethodHandles.Lookup lookup | ||
) { | ||
Function<Object[], R> builder = recordBuilder(recordClass, lookup); | ||
return new ConstructingObjectParser<>(name, ignoreUnknownFields, (args, context) -> builder.apply(args)); |
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.
(For the future... if this were able to consume the context
argument too, there would be a lot more cases where it would apply. The current one applies only to cases where context
is ignored, which seems to be roughly half the places where records are used.)
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.
Actually, looking into this some more, my own CPS requirements actually need this functionality, so that means this PR is insufficient for my own needs in its current form.
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.
Love the idea of improving this, this is awful at the moment!
Looks good, though I suggest to restructure the code a little bit.
I'm not sure how much we have to worry about performance, but if recall right parsing has come up repeatedly in flame graphs. On the other hand, that was mostly chunking I think and none of the converted records look performance critical.
* typically just {@code MethodHandles.lookup()} called by some code that can access the constructor. | ||
* @return a function suitable to use as the {@code builder} argument for one of the constructors of this class. | ||
*/ | ||
public static <R extends Record, Context> ConstructingObjectParser<R, Context> forRecord( |
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.
How about restructuring this a bit for readability and have recordBuilder
return the actual builder BiFunction
as expected by ConstructingObjectParser
using
<R extends Record> MethodHandle recordConstructor(Class<R> recordClass, MethodHandles.Lookup lookup)
and<R extends Record, Context> BiFunction<Object[], Context, R> recordBuilder(MethodHandle ctor)
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.
Yeah I thought the same thing actually. I should do that.
private static Object constructRecord(Object[] args, MethodHandle ctor) { | ||
Object result; | ||
try { | ||
result = ctor.invokeWithArguments(args); |
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.
Typically this area is rather performance sensitive, not sure how much we have to worry here.
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.
Wondering how invoke
on an array-spreading method handle (asSpreader(Object[].class, ctorArgs.length)
) compares to invokeWithArguments
...
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 javadocs say invokeWithArguments
is equivalent to this:
MethodHandle invoker = MethodHandles.spreadInvoker(this.type(), 0);
Object result = invoker.invokeExact(this, arguments);
I could perhaps do the spreadInvoker
ahead of time, leaving only the invokeExact
.
Part of the motivation for this is to enable some performance optimization. By hiding the conversion from |
Motivation
ConstructingObjectParser
requires abuilder
parameter that accepts aObject[]
and constructs the desired object using those as constructor parameters. This leads to boilerplate like this:This is awkward when first developing such a record, when you might add, remove, or rearrange the fields often. It's especially hazardous if two of the arguments have the same datatype, as with the fourth and fifth arguments in the example above, because if you reorder those record components and forget to change this lambda expression, the compiler will not detect the mistake.
And the final insult is, it requires
@SuppressWarnings("unchecked")
if any of the arguments have generic types, likeList<String>
above, meaning you need to opt out of those warnings entirely.Change
ConstructingObjectParser.forRecord
that accepts the record class and returns a parser for that record