Immutability is an important principle of functional programming. Mutable objects hide changes. And hidden changes can lead to unpredictability and chaos.

FunctionalJ provides ways to create and manipulate immutable data. In this article, I discuss @Struct , which generates custom immutable classes. On the surface, it is very similar in concept with Lombok's @Value. However, FunctionalJ's @Struct comes with its own unique features, such as:

Compact form

Non-required fields and default values

Immutable modification

Lens

Exhaustive builder

Validation

Let explore these features!

Note: A companion VDO can be found on youtube.

@Struct

FunctionalJ has a mechanism to create immutable data objects using the @Struct annotation. This can be done in two forms: an expand form and a compact form. The expanded and form allows an opportunity to add additional methods. For brevity, we will use the compact form when possible. The following code shows how to define a struct using the compact form.

package pkg; public class Models { @Struct void Person(String firstName, String lastName) { } }





Notice that @Struct is annotated on Person , which is just a method. I call this annotated method a specification method (a Kotlin and Scala envy!). Specification methods can be in any class or interface. In this case, we put the Person method in the class named Models , which should make it is easy to locate.

With the above code, FunctionalJ generates a class called Person in the same package with this code ( pkg package). This class has two fields: firstName and lastName .

With that, we can instantiate a Person object using its constructor.

val person = new Person("John", "Doe"); assertEquals("Person[firstName: John, lastName: Doe]", person.toString());





Please note that I use Lombok's val for brevity.

Common Methods

Common object methods, such as toString() , hashCode() , and equals(...) , are automatically generated. The code above shows how toString() might return, and the following code demonstrates that hashCode() and equals(...) behave as expected.

val person1 = new Person("John", "Doe"); val person2 = new Person("John", "Doe"); val person3 = new Person("Jane", "Doe"); assertTrue(person1.hashCode() == person2.hashCode()); assertTrue(person1.equals(person2)); assertFalse(person1.hashCode() == person3.hashCode()); assertFalse(person1.equals(person3));





Accessing a Field

The fields can be accessed using its getter, which is just the method with the same name.

val person = new Person("John", "Doe"); assertEquals("John", person.firstName()); assertEquals("Doe", person.lastName());





Changing a Field Value

Since the object is immutable, there is no way to actually change the value of the field in the object. So to change the field value, we create another object with the new field value (I call this "immutable modification" — creating a new instance with the modification). The method withXXX(...) can be used to do just that.

val person1 = new Person("John", "Doe"); val person2 = person1.withLastName("Smith"); assertEquals("Person[firstName: John, lastName: Doe]", person1.toString()); assertEquals("Person[firstName: John, lastName: Smith]", person2.toString());





In the code above, person2 is person1 with the new last name.

Null and Default Values

By default, null is not allowed as the property value. NullPointerException will be thrown if null is given as the field value.

try { new Person("John", null); fail("Expect an NPE."); } catch (NullPointerException e) { }





In order to allow the field to accept null, the field must be annotated with @Nullable ( functionalj.types.Nullable ). So, let's say we add middleName field to the Person class and make it nullable.

@Struct void Person(String firstName, @Nullable String middleName, String lastName) { }





Now, you can use null to specify the middle name.

val person = new Person("John", null, "Doe"); assertEquals("Person[firstName: John, middleName: null, lastName: Doe]", person.toString());





With this nullable field, we got another constructor that only have required fields.

val person = new Person("John", "Doe"); assertEquals("Person[firstName: John, middleName: null, lastName: Doe]", person.toString());





We can also give the fields default values by annotating with DefaultTo(...) . Let's say we want to add age field to the Person class and default it to -1.

@Struct void Person( String firstName, @Nullable String middleName, String lastName, @DefaultTo(DefaultValue.MINUS_ONE) Integer age) { }





So now, we can create person with either a value or null (to use default value).

// With value val person1 = new Person("John", null, "Doe", 30); assertEquals("Person[firstName: John, middleName: null, lastName: Doe, age: 30]", person1.toString()); // With default value val person2 = new Person("John", null, "Doe", null); assertEquals("Person[firstName: John, middleName: null, lastName: Doe, age: -1]", person2.toString());





Of course, the constructor with only the required field is still there.

val person = new Person("John", "Doe"); assertEquals("Person[firstName: John, middleName: null, lastName: Doe]", person.toString());





Lens

A lens is a function that allows access to a field for both reading and changing (using withXXX(...) ). Just like functions in FunctionalJ, lenses are greatly composable — you can use it to access deep into the sub-object. Consider the following code:

@Struct void Employee( String firstName, @Nullable String middleName, String lastName) { } @Struct void Department( String name, Employee manager) { };





Now, you can use the lens to access the field in employee.

import static pkg.Employee.theEmployee; ... val employee1 = new Employee("John", "Doe"); assertEquals("John", theEmployee.firstName.apply(employee1)); assertEquals("Doe", theEmployee.lastName .apply(employee1)); val employee2 = theEmployee.firstName.changeTo("Jonathan").apply(employee1); assertEquals("Employee[firstName: Jonathan, middleName: null, lastName: Doe]", employee2.toString());





Notice the static import for theEmployee . In other words, the lens is created as a static final field of the generated class.

Using lenses, it is possible to quickly access a field in the employee from the department.

import static pkg.Department.theDepartment; import static pkg.Employee.theEmployee; ... val employee = new Employee("John", "Doe"); val department = new Department("Sales", employee); assertEquals( "Department[name: Sales, manager: Employee[firstName: John, middleName: null, lastName: Doe]]", department.toString()); // Read assertEquals("John", theDepartment.manager.firstName.apply(department)); assertEquals("Doe", theDepartment.manager.lastName .apply(department)); // Change val department2 = theDepartment.manager.firstName.changeTo("Jonathan").apply(department); assertEquals( "Department[name: Sales, manager: Employee[firstName: Jonathan, middleName: null, lastName: Doe]]", department2.toString());





This is more useful when using it with stream or FuncList . The following code extracts the list of manager family name.

val departments = FuncList.of( new Department("Sales", new Employee("John", "Doe")), new Department("R-and-D", new Employee("John", "Jackson")), new Department("Support", new Employee("Jack", "Johnson")) ); assertEquals("[Doe, Jackson, Johnson]", departments.map(theDepartment.manager.lastName).toString());





Another example code gets the list of the department name with the manager last name but only when his name is "John."

val departments = FuncList.of( new Department("Sales", new Employee("John", "Doe")), new Department("R-and-D", new Employee("John", "Jackson")), new Department("Support", new Employee("Jack", "Johnson")) ); assertEquals("[(Sales,Doe), (R-and-D,Jackson)]", departments .filter (theDepartment.manager.firstName.thatEquals("John")) .mapTuple(theDepartment.name, theDepartment.manager.lastName) .toString());





See "Access and Lens" in this link for more detail.

Builder

A struct is also comes with a builder. This builder is exhaustive, meaning that all require fields are provided.

val person = new Person.Builder() .firstName("John") .lastName("Doe") .build(); assertEquals("Person[firstName: John, middleName: null, lastName: Doe, age: -1]", person.toString());





You can also put in non-required fields.

val person = new Person.Builder() .firstName ("John") .middleName("F") .lastName ("Doe") .build(); assertEquals("Person[firstName: John, middleName: F, lastName: Doe, age: -1]", person.toString());





Using the builder makes it easy to see the name of the field and its value. The exhaustive builder can help reduce a mistake, as a compilation error will be raised when non-required fields are not given (like in the case of a newly added field). One limitation of this is that the fields must be given in order specified in the specification method.

Validation

Without setters, there is no direct way to ensure that the new value given is valid. This can be a big problem as the object might become inconsistent. To solve that, FunctionalJ makes it easy to ensure that the instantiated objects are valid. It provides three ways of doing validation for @struct . These ways differ in the way the exception is created.

The first way is to have the spec method return boolean, indicating if the parameters are all valid.

@Struct static boolean Circle(int x, int y, int radius) { return radius > 0; } val validCircle = new Circle(10, 10, 10); assertEquals("Circle[x: 10, y: 10, radius: 10]", validCircle.toString()); try { val invalidCircle = new Circle(10, 10, -10); fail("Except a ValidationException."); } catch (ValidationException e) { assertEquals( "functionalj.result.ValidationException: Circle: Circle[x: 10, y: 10, radius: -10]", e.toString()); }





Notice that the specification method Circle now returns boolean and is static. It is made static because the generated class will call this method.

If the radius is not negative, the circle is created without any problem. If the radius, on the other hand, is negative, a ValidationException is thrown with an automatically-generated message. This should be sufficient in most cases.

If a custom message is needed, the second way can be used, and that is to make the specification method return a String message of the problem or null when valid.

@Struct static String Circle(int x, int y, int radius) { return radius > 0 ? null : "Radius cannot be less than zero: " + radius; } try { new Circle(10, 10, -10); fail("Except a ValidationException."); } catch (ValidationException e) { assertEquals( "functionalj.result.ValidationException: Radius cannot be less than zero: -10", e.toString()); }





In this case, a ValidationException with the message returned by the specification method is thrown when the struct is invalid. If this is still not enough, for example, you want to return custom exception type, the third way can be utilized.

@Struct static ValidationException Circle3(int x, int y, int radius) { return radius > 0 ? null : new NegativeRadiusException(radius); } @SuppressWarnings("serial") public class NegativeRadiusException extends ValidationException { public NegativeRadiusException(int radius) { super("Radius: " + radius); } } try { new Circle3(10, 10, -10); fail("Except a ValidationException."); } catch (ValidationException e) { assertEquals( "pkg.NegativeRadiusException: Radius: -10", e.toString()); }





Additional Functionalities

So far, we only generate a struct class that only has value and default methods. If there is a need for additional methods or to make the generated class extending or implementing some classes/interfaces, we will need to use the extended form.

For example, an abstract class called Greeter that can greet people.

public abstract class Greeter { public abstract String greetWord(); public String greeting(String name) { return greetWord() + " " + name + "!"; } }





Then, you can create a type spec that extends Greeter .

@Struct static abstract class FriendlyGuySpec extends Greeter { public abstract String greetWord(); }





This will generate a class called FriendlyGuy in the same package (the name will be from the specification class name less "Spec" or "Model"). The generated class FriendlyGuy extends Greeter and inherits all methods.

Greeter fiendlyGuy = new FriendlyGuy("Hi"); assertEquals("Hi Bruce Wayne!", fiendlyGuy.greeting("Bruce Wayne"));





New methods can be added to the generated class by just adding them to the specification class.

@Struct static abstract class FriendlyGuySpec extends Greeter { public abstract String greetWord(); public void shakeHand() { ... } }





Basically, the generated class FiendlyGuy extends FriendlyGuySpec , which the intern extends Greeter.

Conclusion

With @Struct , it is much easier to have immutable data classes. There is now no excuse to not use immutable data. Also, I found myself writing a component in one Java file more and more, with all the necessary data classes in that one file, which is much easier to comprehend. I hope you find these functionalities useful and any feedback is always welcome!

Happy coding!