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:

Records are:

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 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 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.

"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.

The Example Code

#!ek9
defines module introduction
  defines type

    Index as Integer constrain as
      > 0

  defines record

    IdRecord as abstract
      id as Index: Index()
      createdAt as DateTime: SystemClock().dateTime()

      IdRecord()
        -> id as Index
        assert id?
        this.id: id

      operator $
        <- rtn as String: $id + " " + $createdAt
      operator ?
        <- rtn as Boolean: id?
      operator ==
        -> item as IdRecord
        <- rtn as Boolean: Boolean()
        if item?
          rtn: item.id == this.id
      operator <>
        -> item as IdRecord
        <- rtn as Boolean: not (item == this)

    CustomerDetail extends IdRecord
      firstName as String: String()
      lastName as String: String()
      dateOfBirth as Date: Date()

      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 <~>
        -> 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 <=>
        -> item as CustomerDetail
        <- rtn as Integer: 0

        rtn: dateOfBirth <=> item.dateOfBirth
        rtn += lastName <=> item.lastName
        rtn += firstName <=> item.firstName

      operator <
        -> item as CustomerDetail
        <- rtn as Boolean: item <=> this < 0
      operator >
        -> item as CustomerDetail
        <- rtn as Boolean: item <=> this > 0
      operator <=
        -> item as CustomerDetail
        <- rtn as Boolean: item <=> this <= 0
      operator >=
        -> item as CustomerDetail
        <- rtn as Boolean: item <=> this >= 0
      operator #^
        <- rtn as String: $this

      override operator $
        <- rtn as String: $super + " " + firstName + " " + lastName + " " + $dateOfBirth
      override operator ?
        <- 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'.

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. If you are keen on pre and post conditions then using assert is a good idea (it is not spectific to records - it can be used anywhere). 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.

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.

Next Steps

The next section shows how functions can be both abstract and polymorphic.