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);
>   }
>
>
>
>
>
>
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20200523/0d4d2a22/attachment.htm>


More information about the amber-spec-experts mailing list