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 <fo...@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); > } > > > > > > > > >