Re-using fields across multiple records

Hello all,

I have a problem using Dhall for configs for which I’ve not found an ideal approach, maybe someone here has encountered something similar.

We are storing multiple records, which share common fields, something like this:

let EmployeeRecord = {
    name : Text
    , height : Double

let CustomerRecord = {
    name : Text
    , age : Natural

These records are in reality long and change often, and have proved to be difficult-to-maintain code, so it’s important that there is only one list of fields per record stored in code, ie there is no duplication of the record’s field list, at least not in a way that can be messed up.

It’s then necessary to convert these to other formats, eg .csv.

What I would like to be able to do is to:

  1. Have a shared list of common fields (in this case, name is common between the two records) and store some metadata about these fields themselves.
  2. Find a way to write accessory functions (for example, to convert these records to .csv) in such a way that these accessory functions don’t require the records’ fields to be repeated within the body of the function, or leave other room for error.

One approach which serves objectives 1. and 2. well is to use a union type that includes all fields across all of these records:

let PersonDetail = <
    | name : Text
    | height : Double
    | age : Natural

let EmployeeRecord = {
    name : PersonDetail
    , height : PersonDetail

Then to achieve type safety, EmployeeRecords are not created directly, but via a constructor function:

let get_employee =
    λ(name : Text) ->
    λ(height : Double) -> {
        name = name
        , height = PersonDetail.height height
    } : EmployeeRecord

This also allows the functions which create .csvs (shown directly below) to be pretty straightforward. The function below works well because there’s no room for the code maintainer to make a mistake by missing out one of EmployeeRecord's fields, if they were typing them by hand. This approach only works because all of EmployeeRecord's fields have the same type PersonDetails. If it was a normal-looking record then toMap could not be used and the fields would have to be repeated by hand, something I’m keen to avoid.

let to_text =
    λ(person_detail : PersonDetail) -> merge {
        name = λ(x : Text) -> x
        , height = λ(x : Double) -> Double/show x
        , age = λ(x : Natural) -> Natural/show x
    } person_detail
    : Text

let employee_to_csv_line =
    λ(employee : EmployeeRecord) ->
        let field_map = toMap employee
        let DetailMapType = { mapKey : Text , mapValue : PersonDetail }
        let map_to_text = λ(x : DetailMapType ) -> to_text x.mapValue
        in Prelude.Text.concatMapSep

and so this gets me quite close to my goal. Using a get_employee constructor rather than constructing EmployeeRecord directly feels a bit peculiar but has the same type safety so I can’t really complain.

This does make functions to recover metadata about the fields themselves a bit awkward to call, though, in that I have to make up a dummy value for the PersonDetail type:

let get_person_detail_metadata =
    λ(person_detail : PersonDetail) -> merge {
        name = λ(x : Text) -> "Name of person"
        , height = λ(x : Double) -> "Height of person"
        , age = λ(x : Natural) -> "Age of person"
    } person_detail
    : Text

let name_metadata = get_person_detail_metadata 
    ( "this text is irrelevant!")

Having written all this I’ve come to realize that I’ve not really got a direct question to ask, but if anyone’s dealt with a similar situation to this, or found a better paradigm for it, I’d be glad to hear.

You might be interested in the //\\ / operator for combining record types, so you can factor out shared fields in a record type like this:

let Shared = { name : Text }

let EmployeeRecord = Shared ⩓ { height : Double }

let CustomerRecord = Shared ⩓ { age : Natural }