Records in EK9
The Record was briefly outlined in the record section in 'structure'. It is shown in more detail here in the form of an example.
Uses for Records
Records are not:
- the same as the Java 14 record
- a representation of a database row (though you could use them like this if you wish)
- immutable (read only) - the data in EK9 records can be changed
- able to have additional methods applied to them - though you can/should implement the operators
- limited - as they can extend other records and can be open for extension
Records are:
- really just a 'data transfer object' in some ways
- able to be defined as being abstract
- quite similar to a 'C' struct - but can have operators
- ideal for passing multiple values in and out of methods and functions
- able to define one or more constructors
Data Transfer Object?
Records can be useful in data processing as shown in the standard types worked example. In that example there were two sources of 'customer record' that had to be merged and output. You might argue that the record is like an anemic class. In some ways they can be! After all transmission of data structures or their storage in to a database/file means that no 'behaviour' is transmitted or stored.
But with EK9; the record is designed to be anemic (or maybe slightly anemic). In general the definition of constructors/operators on a record, does provide behaviour and functionality. But in a very limited and controlled manner. If you need more than this; then use a class.
But in EK9 it is not frowned upon or considered poor style to use a 'Data Transfer Object' and 'do operations' on its public data with functions. That is what it is for.
You may consider altering state variables within a record a dangerous and risky operation, after all
mutation of aggregate data should be controlled in some way. This is why many languages have the concept of
immutable data structures (to stop such mutation).
EK9 does not have the concept of an immutable or final or const modifier (it does have
constants however). EK9 approaches immutability in a different way.
EK9 employs the pure method/function modifier - this indicates that any data
passed in to the method/function that is marked as pure cannot be mutated.
It is the mechanism of pure that enables any data structure to remain un-mutated. This also encourages the use of Streams/Pipelines and cloning/copying of data (then altering the new data structure during such processing).
"It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures." - Alan Perlis (SICP Foreword). You may or may not agree with these thoughts and ideas - but they are a valid approach in software development.
If you prefer full data encapsulation and a rich object model, use classes and components and leave out records.
The Power of Operators
When you review the example below with the operators that have been defined; you will probably agree that the record is not that 'anemic'. It does provide quite a bit of behaviour in this example. The approach to coding up records and classes with a fairly full set of operators/functionality is still quite popular with developers of some languages like C++, C# and Scala. But as Java has no operators the development of more fully rounded classes has fallen out of favour.
For long-lived software; developing a fully rounded set of types, such as records and classes really pays off in the long run. They don't have to be developed in a fully featured manner right from the start. It is sometimes best to take more of a minimalist approach and only define the operators and methods as you need them.
For some developers the pleasure of developing a fully rounded type and an associated set of unit tests as examples of how it can be used, gives a sense of achievement. This is particularly the case for more junior developers, where they can be given a straight forward and definitive task by more senior staff/architects.
EK9 can provide a number of operators by default with a simple directive:
- //EK9 to provide default implementations
- default operator
EK9 can add '$', '$$', '#?', '?' and '==', '<>', '<=>', '<', '<=', '>', >=' by using the fields present in the record. But you can also provide your own implementations if you wish and EK9 will add in those you did not implement.
The Example Code
#!ek9 defines module introduction defines type Index as Integer constrain as > 0 defines function defaultDateOfBirth() <- rtn as Date: Date() defines record IdRecord as abstract //We can support simple inference, but not complex expressions for fields/properties. id <- Index() createdAt as DateTime: SystemClock().dateTime() IdRecord() -> id as Index assert id? this.id: id operator $ as pure <- rtn as String: $id + " " + $createdAt operator ? as pure <- rtn as Boolean: id? operator == as pure -> item as IdRecord <- rtn as Boolean: Boolean() if item? rtn: item.id == this.id operator <> as pure -> item as IdRecord <- rtn as Boolean: not (item == this) CustomerDetail extends IdRecord firstName as String: String() lastName as String: String() dateOfBirth as Date: defaultDateOfBirth() CustomerDetail() -> id as Index firstName as String lastName as String super(id) assert firstName? assert lastName? this.firstName: firstName this.lastName: lastName CustomerDetail() -> id as Index dateOfBirth as Date super(id) assert dateOfBirth? this.dateOfBirth: dateOfBirth CustomerDetail() -> id as Index firstName as String lastName as String dateOfBirth as Date super(id) assert firstName? assert lastName? assert dateOfBirth? this.firstName: firstName this.lastName: lastName this.dateOfBirth: dateOfBirth operator :=: -> item as CustomerDetail id :=: item.id firstName :=: item.firstName lastName :=: item.lastName dateOfBirth :=: item.dateOfBirth operator :~: -> item as CustomerDetail if not id? id :=: item.id if not firstName? firstName :=: item.firstName if not lastName? lastName :=: item.lastName if not dateOfBirth? dateOfBirth :=: item.dateOfBirth operator <~> as pure -> item as CustomerDetail <- rtn as Integer: 0 //Use a fuzzy match on string version of date of birth first rtn: $this.dateOfBirth <~> $item.dateOfBirth rtn += this.lastName <~> item.lastName rtn += this.firstName <~> item.firstName operator <=> as pure -> item as CustomerDetail <- rtn as Integer: 0 rtn: dateOfBirth <=> item.dateOfBirth rtn += lastName <=> item.lastName rtn += firstName <=> item.firstName operator < as pure -> item as CustomerDetail <- rtn as Boolean: item <=> this < 0 operator > as pure -> item as CustomerDetail <- rtn as Boolean: item <=> this > 0 operator <= as pure -> item as CustomerDetail <- rtn as Boolean: item <=> this <= 0 operator >= as pure -> item as CustomerDetail <- rtn as Boolean: item <=> this >= 0 operator #^ as pure <- rtn as String: $this override operator $ as pure <- rtn as String: $super + " " + firstName + " " + lastName + " " + $dateOfBirth override operator ? as pure <- rtn as Boolean: super? and firstName? and lastName? and dateOfBirth? defines program ShowCustomerRecords() stdout <- Stdout() unknownCustomer <- CustomerDetail() assert ~unknownCustomer? try invalidCustomer <- CustomerDetail(Index(-1), "", "", Date()) catch -> ex as Exception stdout.println("As expected Index cannot be less than zero " + $ex) try invalidCustomer <- CustomerDetail(Index(1), String(), String(), Date()) catch -> ex as Exception stdout.println("As expected details must be set " + $ex) customer1 <- CustomerDetail(Index(1), "Gomez", "Addams", 1963-06-08) customer2 <- CustomerDetail(Index(2), "Morticia", "Addams", 1965-01-03) customer3 <- CustomerDetail(Index(3), "Pugsley", "Addams", 1984-10-21) assert customer1 <> customer2 stdout.println("Three Addams, " + customer1.firstName + " " + customer2.firstName + " and " + customer3.firstName) //Some Addams we've not see before but born on same day or typo of Gomez? customer4 <- CustomerDetail(Index(4), "Goetez", "Addams", 1963-06-08) comp1 <- customer4 <=> customer1 comp2 <- customer4 <=> customer2 comp3 <- customer4 <=> customer3 stdout.println("Compares [" + $comp1 + " " + $comp2 + " " + $comp3 + "]") best <- customer4 <~> customer1 < customer4 <~> customer2 <- customer1 else customer2 best: customer4 <~> customer3 < customer4 <~> best <- customer3 else best //full copy of the details unknownCustomer :=: best //now while objects differ contents are the same. assert unknownCustomer <=> customer1 == 0 partialCustomer1 <- CustomerDetail(Index(1), "Gomez", "Addams") partialCustomer2 <- CustomerDetail(Index(1), 1963-06-08) //As both are partial the result will be unset! assert not (partialCustomer1 <=> partialCustomer2)? partialCustomer1 :~: partialCustomer2 //After merging the two should match Gomez. assert partialCustomer1 <=> customer1 == 0 //EOF
Constraints
The first bit of new syntax is the mechanism EK9 has to limit or constrain a type. Here Index is an Integer but can only have a value greater than zero. The Index type is then used as the type of the 'id' property in the record 'IdRecord'.
Index is a new type - it is not interchangeable with an Integer and does not extend and Integer.
The Records
This example shows two records, the 'IdRecord' and 'CustomerDetail'. The 'IdRecord' is straight forward; with just two fields/properties and a number of operators. However, it is abstract; meaning that it cannot be instantiated in of itself.
The 'CustomerDetail' record has a number of different constructors and quite a wide range of operators. Read up on operators if any of these are unfamiliar. This record makes good use of the copy, merge and fuzzy match operators but also includes the comparison operators. As you can see there is quite a bit of functionality and behaviour in this record
Using the CustomerDetail record
The program 'ShowCustomerRecords' is a small example to illustrate how the record can be used. Note the fact that the record fields/properties are public.
Exceptions
There are a couple of try/catch blocks shown in the example. The first is to catch the fact that the program tries to create a 'CustomerDetail' object with an id of '-1'. The second try/catch block is to catch the Exception that is thrown in the 'CustomerDetail' constructor when the assert statement is used. It will throw and Exception if the expression in the assertion returns false. So it is very strict and will bring processing to a halt unless the Exceptions are caught.
The alternative to using Exceptions is to leave each of the objects unset and handle that aspect in the calling code.
Using the Operators
What follows in the example is a range of different statements involving the operators implemented in the record. The fuzzy match (<~>) is used in conjunction with a ternary operator to find the best match for a 'CustomerDetail' Record (first name - 'Goetez').
Finally the merge (:~:) operator is used to merge two partially records together and that is then compared to the 'customer1' (Gomez Addams) record.
Summary
The example above is fairly long and has a couple of bits of syntax not seen before. But hopefully it demonstrates how records can provide quite a significant amount of functionality and are a little more than just simple Data Transfer Objects. By enabling the use of a finite set of operators EK9 gives records a very specific, but valuable role in any solution developed.
If you are a more functional programmer, then records and functions are pretty much perfect when linked with the pure key word. For Object-Oriented programmers, maybe the inclusion of operators on a 'DTO' will be useful, but there are traits and classes yet to come.
A more functional approach would probably not employ Exceptions and would use the unset nature included in EK9 as these would work well with Streams/Pipelines.
Next Steps
The next section shows how functions can be both abstract and polymorphic.