val emily2 = emily1.newBuilder()
.lastName("Wells")
.age(26)
.build()
I think the inner Builder pattern is common enough in immutable java object implementations that they might want to include that fully in the Record class generation at some point. Most immutable object libraries that I've seen include a lot more functionality than Records, like builders. val emily2 = { ...emily1, age: 30 };
This has many advantages, one being that it provably and declaratively creates a copy, which is not the case with the builder. In fact the obsession with methods (and hence the implied state) is what makes Java a terrible misfit for functional paradigms such as immutability. // Java
val emily2 = emily1.toBuilder().age(30).build()
As far as adding the spread operator to Java, I think you'd have to require a Spreadable interface to use it or limit usage of the spread operators to records. It's not clear to me that the expressive power is worth it over a builder.Otherwise, at some point you need to add a field to Person, and then the compiler can’t tell you about all the places where your code fails to initialize the field.
Worse, some people pass builders around.
At that point, the problem of checking that you’ve passed a legal set of arguments when constructing objects is as hard as the halting problem.
You can write tests to make sure your program is typesafe, but that takes time away from writing more useful tests.
Optional arguments with reasonable defaults don't present a problem for refactoring. It does make it hard to spot the cases where the reasonable defaults don't work. One trick is to temporarily add it to the constructor, review all the errors, and then remove the constructor arg.
Kotlin has a very similar feature in its data classes, FWIW.
I really wish Java would add both default arguments for functions, and also support for passing function arguments by name.
One of these days...
Writing simple methods with few arguments helps.
Kotlin provides named arguments and a copy method on data classes/records that uses them. You do indeed have binary compatibility issues with them, but it's not a fundamental problem. It's just that it sits at the intersection of:
- Problems that only affect library writers, rarely a high priority for language designers (Java being a exception)
- Obscure JVM knowledge and new techniques
- Poor Android JVM holding back the ecosystem by discouraging any bytecode techniques that post-date Java circa 2010.
Ok, it cheats a little bit because named params are actually a syntax sugar for Maps. But other languages do the same (e.g. Python) and is very useful too. And if you can type check the map (as in Typescript) the functionality is equivalent to named params.
[1] http://docs.groovy-lang.org/latest/html/documentation/#_name...
Ruby before 2.7 has a syntax sugar where keyword parameters are the same as a trailing hash. But that's changing as well.
The kotlinx.serialisation design is pretty nice. It's basically what you describe. The compiler generates generic code that can call into a variety of "codecs" but they don't have to actually be serialisation specific. So there are codecs for JSON, protobufs, CBOR etc but you can also do arbitrary object graph transformations with the framework. A deep clone being a simple hello-world type example. I've never used Shapeless but it sounds pretty similar in some ways.
So what does the value that gets passed into your codec look like? What makes Shapeless tick is that it can represent records generically at the type level; records will have a type like
type Book = 'author ->> String :: 'title ->> String :: HNil
that you can actually break down and do meaningful operations on (e.g. you could run it through a function that counts the lengths of strings and get something of type 'author ->> Int :: 'title ->> Int :: HNil, in a fully type-safe way). I'd be impressed if someone managed to encode those kinds of types into Kotlin - e.g. I don't think Kotlin has singleton types, so being able to compare field names at the type level is probably impossible?Alternatively, they could automatically generate a builder class for records.
As always in java, I guess that somebody will duct tape an annotation based solution on top of the language to solve this.
F#: let updated = { old with prop = newValue }
Elixir: update = %{ old | prop: newValue }
Even JavaScript have similar stuff: const update = { ...old, prop: newValue }
just a fn in the end
The assumption of inputs based on the order of the class items frightens me!
val emily1 = Person(firstName="Emily", lastName="Maness", age=25)
Scala's approach to constructors is a good example of convention over configuration; in Java 99.9% of classes have a constructor like: this.firstName = firstName;
this.lastName = lastName;
this.age = age;
which is just ceremonial boilerplate that obscures the actual logic of what the class is doing (an IDE can generate the constructor for you, but the person maintaining your code still has to take the time to comprehend it). You can do the more complicated things in cases where you need to, but it's important to keep the simple case simple.It's a compromise.