Record types

If we want to give the fields in a data constructor their own names, we can use Unison's syntax for record types. When you define a record type, Unison will automatically generate functions which allow you to set a value of the data type, to get a value from the data type, and modify a value in the data type.

Let's add a record type to model the idea of a volunteer.

unique type Volunteer
  = { preferredName : Text,
    age : Nat,
    desiredStartDate : Text,
    endDate : Optional Text,
    daysAvailable : Set DaysOfWeek }

Record types are started with the name of the type, but instead of a data constructor, you'll see open curly braces to the right of the equals sign. Each field name is named and given a type in a comma-separated list. Record types are supported for types which have a single data constructor. Unison creates a data constructor for you with the same name as the overall type.

Imagine if we had to remember what each field meant for Volunteer without these names! 😵 Record types can be useful when the data constructor for a type gets exceptionally long, and therefore component parts would benefit from receiving a human readable name.

Upon saving the scratch.u file, we can see that Unison has derived a series of functions for each of our named fields.

⍟ These new definitions are ok to `add`:

      type DaysOfWeek
      type Volunteer
      Volunteer.age                     : Volunteer -> Nat
      Volunteer.age.modify              : (Nat ->{g} Nat) -> Volunteer ->{g} Volunteer
      Volunteer.age.set                 : Nat -> Volunteer -> Volunteer
      Volunteer.daysAvailable           : Volunteer -> Set DaysOfWeek
      Volunteer.daysAvailable.modify    : (Set DaysOfWeek ->{g} Set DaysOfWeek)
                                          -> Volunteer
                                          ->{g} Volunteer
      Volunteer.daysAvailable.set       : Set DaysOfWeek -> Volunteer -> Volunteer
      Volunteer.desiredStartDate        : Volunteer -> Text
      Volunteer.desiredStartDate.modify : (Text ->{g} Text) -> Volunteer ->{g} Volunteer
      Volunteer.desiredStartDate.set    : Text -> Volunteer -> Volunteer
      Volunteer.endDate                 : Volunteer -> Optional Text
      Volunteer.endDate.modify          : (Optional Text ->{g} Optional Text)
                                          -> Volunteer
                                          ->{g} Volunteer
      Volunteer.endDate.set             : Optional Text -> Volunteer -> Volunteer
      Volunteer.preferredName           : Volunteer -> Text
      Volunteer.preferredName.modify    : (Text ->{g} Text) -> Volunteer ->{g} Volunteer
      Volunteer.preferredName.set       : Text -> Volunteer -> Volunteer

Creating a value for a Volunteer doesn't involve any additional overhead. There's no requirement for mentioning the field names.

But with these programmatically generated methods, we can easily change the value of one field:

Here we've simply overwritten the original value for our example's age field. The first argument to Volunteer.age.set is your desired value of the field and the second argument is the original Volunteer. The return type of our set function is Volunteer, and remember, Unison values are immutable, so this is a copy of the original Volunteer.

If we didn't have a specific value to set, we can provide a function which describes how one field should be transformed using the modify function which is generated for each field in a record.

Here we're using the Volunteer.age.modify function created for the record's age. Checkout the signature Volunteer.age.modify : (Nat ->{g} Nat) -> Volunteer ->{g} Volunteer and note that you couldn't, for example, use the modify function to change from an age expressed in terms of Nat to an age expressed in terms of some type representing Geologic Time. 🦕

Using the record type syntax we can also streamline the process of getting the value of one field. Here's what getting the value for our example's age looks like using the Volunteer.age function that Unison provides for us.

Compare this to having to pattern match on the value to get a value:

getAge : Volunteer -> Nat
getAge = cases Volunteer _ age _ _ _ -> age
getAge example

Of course, you can still pattern match on a record type--you do not need to mention the field names in your pattern match cases--those conventions don't change.

🌻

Summary:

  • Record types allow you to define a type with individually named field names
  • Record types are for types with a single data constructor
  • Record types automatically generate functions which get, set, and modify values for the record