- Notifications
You must be signed in to change notification settings - Fork0
A Java library that puts a new spin on the newly introduced record classes.
License
crschnick/remix
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Remix is a new lightweight Java library that provides useful features for thenewly introducedrecord classes, which are planned to release with JDK 16.These features currently include:
- Record builders
- Record blanks
- Mutable components
- Copies and deep copies
- Structural copies
- Pattern binding
Note that this library is still in early development and will change quite drastically.The goal is to release version 1.0 of this library when JDK 16 is in itsfinal phase.
While there already exist libraries that provide many of the same features as record classes and Remix, likeAuto orLombok,there are two significant differences:
Remix requires no annotation processor and therefore works outof the box when adding it as a dependency to your project
Remix focuses exclusively on records and is therefore able to exploit everysingle aspect of records better than a library with a more general focus.Many features that remix provides are only possible because of the strict requirements for record classes.
To use Remix and record classes, you must use a build ofJDK 15 withpreview features enabled.To use Remix with Maven you have to add it as a dependency:
<dependency> <groupId>org.monospark</groupId> <artifactId>remix</artifactId> <version>0.2</version></dependency>
You also have to enable preview features:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <release>15</release> <compilerArgs>--enable-preview</compilerArgs> </configuration></plugin>
For gradle, add the following entries to your build.gradle file:
dependencies { implementation group: 'org.monospark', name: 'remix', version: '0.2'}tasks.withType(JavaCompile) { options.compilerArgs += "--enable-preview"}
With Remix, record instances can be constructed using the well-established Builder pattern.Let's define the following example record class:
public record Car(String manufacturer, String model, int price, boolean available) {}
You can then create a Car instance as follows:
Car car = Records.builder(Car.class) .set(Car::manufacturer).to(() -> "RemixCars") .set(Car::model).to(() -> "The Budget car") .set(Car::price).to(() -> 10000) .set(Car::available).to(() -> true) .build();
By default, every component is assigned its default value,i.e. null for objects and 0, 0.0, false, etc. for primitives.
// model and manufacturer are null, price is 0, available is falseCar defaultCar = Records.builder(Car.class).build();
You can modify this behaviour by specifying a global default value blank as shown next.
In addition, these builders also enable you to create record blanks,which are basically blueprints to create new instances of some record class.These blank records can also be reused to effectively implement default values for record components:
// We only sell cars manufactured by us, so let's predefine the manufacturerRecordBlank<Car> carBlank = Records.builder(Car.class) .set(Car::manufacturer).to(() -> "RemixCars") .blank(); // No need to specify the manufacturer since the builder takes already set values from the blankCar c2 = Records.builder(carBlank) .set(Car::model).to(() -> "The luxurious car") .set(Car::price).to(() -> 60000) .set(Car::available).to(() -> true) .build();
By creating a Remix object, we are able to define a global default blank,that will be used by every created builder as follows:
@Remixpublic record Car(String manufacturer, String model, int price, boolean available) { private static void createRemix(RecordRemix<Car> r) { r.blank(b -> b.set(Car::manufacturer).to(() -> "RemixCars")); }}
Now, every builder create withRecords.builder()
automatically sets the manufacturer.Next, we will see how Remix objects can be used for input validation and more.
Remix also provides mutable wrappers that enable you to modify recordcomponents that would otherwise be immutable without violating the basic concepts of records.For example, we can modify the Car record to make the price and availability mutable and add input validation:
@Remixpublic record Car(String manufacturer, String model, MutableInt price, MutableBoolean available) { private static void createRemix(RecordRemix<Car> r) { r.assign(o -> o .notNull(o.all()) .check(Car::price, p -> p > 0) ); }}
We can then modify car instances as follows:
Car car = Records.builder(Car.class) .set(Car::manufacturer).to(() -> "RemixCars") .set(Car::model).to(() -> "The Budget car") .set(Car::price).to(() -> 10000) .set(Car::available).to(() -> true) .build();Records.set(car::available, false);int currentPrice = Records.get(car::price);Records.set(car::price, currentPrice - 1000);// Throws an illegal argument exceptionRecords.set(car::price, -5);
The builder usage stays the same with wrapped components.
One commonly needed feature is copying or cloning record instances.In most cases, using theRecords.copy(...)
method works out of the box.For example, instances of the Car record can easily be copied:
Car copy = Records.copy(car);// Does not change the availability of the original car instanceRecords.set(copy::available, false);
However, this does not work if you want to perform copies of recordsthat have for example have a collection as a record component,since the collection contents are not copied.In this case, deep copies have to be performed to completely decouple copies of original instances.Lets take the following example of copying the car storage:
CarStorage copy = Records.copy(store);var c = Records.get(copy::cars).get(0);// This changes the availability of the car in the original storage as well!Records.set(c::available, false);
If we want to support copying instances of the storage record usingRecords.copy(...)
and want to perform a deep copy, we have to explicitly specify the copy operations.:
@Remixpublic record CarStorage(List<Car> cars, int capacity) { private static void createRemix(RecordRemix<CarStorage> r) { r.assign(o -> o .check(CarStorage::capacity, c -> c > 0) .notNull(CarStorage::cars) .check(CarStorage::cars, c -> c.stream().noneMatch(Objects::isNull)) .add(CarStorage::cars, ArrayList::new) ); // Perform a deep copy! r.copy(o -> o.add(CarStorage::cars, e -> e.stream() .map(Records::copy) .collect(Collectors.toCollection(ArrayList::new)))); } public void addCar(Car car) { if (car == null || cars.size() == capacity) { return; } cars.add(Objects.requireNonNull(car)); }}
This allows us to work on copied storages like this:
CarStorage copy = Records.copy(store);var c = Records.get(copy::cars).get(0);// This does not change the availability of the car in the original storage!Records.set(c::available, false);
Lets take the following simple color record that defines default values and does input validation:
@Remix(Color.Remixer.class)public record Color(int red, int green, int blue) { public static class Remixer implements RecordRemixer<Color> { @Override public void create(RecordRemix<Color> r) { r.blank(b -> b .set(Color::red).to(() -> 0) .set(Color::green).to(() -> 0) .set(Color::blue).to(() -> 0)); Predicate<Integer> range = v -> v >= 0 && v <= Short.MAX_VALUE; r.assign(o -> o.check(o.all(), range)); } }}
When working with already existing records, it is sometimes necessary to create structural copies of records.For example, if we have to work with the following record:
public record OtherColor(int red, int green, int blue) {}
If you are not easily able to modify this record, you can easilyconvert records from one type into another as long as the types arethe same and assignment constraints are not violated:
Color c = Records.create(Color.class, 500, 2032, 2034);OtherColor other = Records.structuralCopy(OtherColor.class, c);Color fromOther = Records.structuralCopy(Color.class, other);
However, if assignment constraints are violated, an exception will be thrown:
OtherColor oc = Records.create(OtherColor.class, -1, -1, -1);// Throws an illegal argument exceptionColor c = Records.structuralCopy(Color.class, oc);
Records are also designed to be used locally in methods.This is useful if you need some kind of custom value storage for only one method.If you want to add some custom behaviour to that local record as well, use can do it like this:
void doStuff() { record TripleEntry(Mutable<String> stringId, MutableInt intId, Object value) {} Records.remix(TripleEntry.class, r -> r.assign(o -> o .notNull(o.all()) .check(TripleEntry::stringId, s -> s.length() >= 5) .check(TripleEntry::intId, i -> i >= 0))); List<TripleEntry> list = new ArrayList<>(); // Do some stuff ...}
Inspired bypattern matching,Remix supports a form a pattern binding.The methodRecords.bind()
can construct bindings as follows:
List<Car> cars = List.of( ... );Function<Car,String> nameFunc = Records.bind(Car::manufacturer).and(Car::model) .toFunction((s1, s2) -> String.join(" ", s1, s2));Map<Car,String> names = cars.stream().collect(Collectors.toMap(c -> c, nameFunc));
This works for consumers, functions and predicates.Record components can also be permuted for bindings.
- Some samples are availablehere
- The javadocs are available atjavadoc.io
- This is a new library, so there will probably be some bugs.If you stumble upon one of them, please report them.
- If you would like to contribute to this project, feel free to do so!Formal contribution guidelines will be coming soon.
About
A Java library that puts a new spin on the newly introduced record classes.