Record copy()/with()
Sergei Egorov
bsideup at gmail.com
Sat May 23 15:03:59 UTC 2020
Hi Remi,
Thanks for raising this important topic!
I wonder if Records can provide the good old
withName()/withAge()/withWhatever() methods and the compiler will either
merge them or we let EA do the rest?
On Sat, May 23, 2020 at 1:36 PM Remi Forax <forax at univ-mlv.fr> wrote:
> I've spent a little time to see how we can provide a with()/copy() method,
> that creates a new instance from an existing record changing the value of
> several components in the process.
>
> It's a feature that was requested several times while i was presenting how
> record works and Scala, Kotlin and C# all provide an equivalent.
> And there is a way to add it without introducing a new syntax.
>
> Syntax in Scala or Kotlin:
> val otherPerson = person.copy(name = "John", age = 17)
>
> Syntax in C#:
> var otherPerson = person with { Name = "John", Age = 17 };
>
> One can note that the syntax in C# doesn't reuse the named argument syntax
> of C#,
> with the named arguments syntax, it should be
> var otherPerson = person.copy(name: "John", age: 17);
>
>
> For Java, one solution is to re-use the same trick used to by
> MethodHandle.invoke*()/VarHandle.*, have a special syntax that is very
> close to the actual syntax so the feature is nicely integrated with the
> rest of the language. Here, the last time we discuss this, we stumble
> because unlike with MethodHandle.invoke*(), the arguments are a key/value
> pairs and there is no existing syntax for that currently in Java.
>
> I believe there possible trick here, use Object... as Map.of() does.
> the idea is to add a method 'Record with(Object... componentValuePairs)'
> in java.lang.Record, and ask the compiler to verify that the even arguments
> (0, 2, 4, etc) are constant strings
>
> Proposed syntax:
> var otherPerson = person.with("name", "John", "age", 17);
>
> Then the compiler translates the method call to an invokedynamic to a
> bootstrap method
> public static CallSite bsm(Lookup lookup, String name, MethodType type,
> String[] componentNames)
> the componentNames being "name" and "age" in the example. The methodType
> has the record as first parameter type and return type, the other
> parameters are the types of the record components corresponding to the
> component names.
>
> In term of separate compilation, the BSM should verify that the component
> names are valid record component and that the method type parameters has
> the same type as the corresponding record component.
> So adding a new record component is a compatible change, removing a record
> component or changing its type is not if a method call to 'with' reference
> it.
>
> In term of Class.getMethod()/Lookup.findVirtual(), I propose to not see
> the method 'with', so it's just a compiler artifact, if someone want the
> method 'with' at runtime, he can call the bootstrap method.
>
> regards,
> Rémi
>
> PS: here is the code for the bootstrap method
>
> ---
> public static CallSite bsm(Lookup lookup, String name, MethodType type,
> String[] componentNames) {
> Objects.requireNonNull(lookup);
> Objects.requireNonNull(name);
> if (type.parameterCount() == 0 || type.returnType() !=
> type.parameterType(0)) {
> throw new IllegalArgumentException("invalid method type " + type);
> }
> if (componentNames.length != type.parameterCount() - 1) { // implicit
> null check
> throw new IllegalArgumentException("wrong number of component names
> ");
> }
>
> HashMap<String, Integer> withIndexMap = new HashMap<>();
> for (int i = 0; i < componentNames.length; i++) {
> String componentName = Objects.requireNonNull(componentNames[i]);
> Object result = withIndexMap.put(componentName, i + 1); // 'this' is
> at position 0
> if (result != null) {
> throw new IllegalArgumentException(
> "component names contains twice the same name " +
> componentName);
> }
> }
>
> Class<?> recordType = type.returnType();
> RecordComponent[] components = recordType.getRecordComponents();
> if (components == null) {
> throw new IllegalArgumentException("the return type is not a record
> " + recordType.getName());
> }
>
> int length = components.length;
> Class<?>[] constructorParameterTypes = new Class<?>[length];
> int[] reorder = new int[length];
> MethodHandle[] filters = new MethodHandle[length];
> for (int i = 0; i < length; i++) {
> RecordComponent component = components[i];
> String componentName = component.getName();
> Class<?> componentType = component.getType();
> constructorParameterTypes[i] = componentType;
> int withIndex = withIndexMap.getOrDefault(componentName, -1);
> // a record component value either comes from the arguments or from
> this + accessor call
> if (withIndex == -1) { // it comes from this + accessor
> try {
> filters[i] = lookup.unreflect(component.getAccessor());
> } catch (IllegalAccessException e) {
> throw (IllegalAccessError) new IllegalAccessError().initCause(e);
> }
> // and reorder[i] == 0
> } else { // it comes from the arguments
> reorder[i] = withIndex;
> if (type.parameterType(withIndex) != componentType) {
> throw new IncompatibleClassChangeError(
> "invalid parameter type "
> + componentType
> + " at "
> + withIndex
> + " for component name "
> + componentName);
> }
> withIndexMap.remove(componentName); // mark that the component
> name has been visited
> // and filter[i] == null
> }
> }
>
> if (!withIndexMap.isEmpty()) { // some component names do not exist
> throw new IncompatibleClassChangeError("invalid component names " +
> withIndexMap.keySet());
> }
>
> MethodHandle constructor;
> try {
> constructor = lookup.findConstructor(recordType,
> MethodType.methodType(void.class, constructorParameterTypes));
> } catch (NoSuchMethodException e) {
> throw (NoSuchMethodError) new NoSuchMethodError().initCause(e);
> } catch (IllegalAccessException e) {
> throw (IllegalAccessError) new IllegalAccessError().initCause(e);
> }
> MethodHandle filtered = MethodHandles.filterArguments(constructor, 0,
> filters);
> MethodHandle target = MethodHandles.permuteArguments(filtered, type,
> reorder);
> return new ConstantCallSite(target);
> }
>
>
>
>
>
>
>
>
>
More information about the amber-spec-observers
mailing list