Record copy()/with()
Jordan Zimmerman
jordan at jordanzimmerman.com
Sat May 23 14:47:06 UTC 2020
Please don't do this. This feels like a hack and loses the elegance of Records. Java has had the time to implement Records (and upcoming pattern matching) such that it will be best-in-class. This way of supporting copy feels backwards and inferior. If Java will add this feature it should be a first class feature. What people want is something like:
record Person(String name, int age){}
var p = new Person("Foo Bar", 42);
var copy = Person(p).age(20).build(); // the terminating method name is unimportant
I'm sure the Amber team is aware of this. Note that this can be done already with an annotation processor (I wrote one: https://github.com/Randgalt/record-builder <https://github.com/Randgalt/record-builder>). I wrote to the Amber team before on this. If you don't address this properly now you will leave a vacuum where people will do their own implementations and we will end up with a plethora of incompatible and varying quality solutions. I hope that the Amber team will address the need for this before the final version is released.
-Jordan
> From: Remi Forax <forax at univ-mlv.fr>
> Subject: Record copy()/with()
> Date: May 23, 2020 at 6:35:42 AM EST
> To: amber-spec-experts <amber-spec-experts at openjdk.java.net>
> Reply-To: Amber Expert Group Observers <amber-spec-observers at openjdk.java.net>
>
>
> 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