views:

19

answers:

1

A simple question I suspect. I have the simple function definition

makePatientFixture :: [ { name :: String, age :: Int} ];
makePatientFixture = [ { name = "Dave", age = 41}, { name = "Denise", age = 45}, { name = "Cameron", age = 5} ];

I actually want to define a new type called

Patient = { name :: String, age :: Int } 

This would mean that I don't have to repeat the record structure all of the time ({ name :: String, age :: Int }) instead my code would look like:

makePatientFixture :: [ Patient ];
makePatientFixture = [ { name = "Dave", age = 41}, { name = "Denise", age = 45}, { name = "Cameron", age = 5} ];

Is this possible? Does it make sense from a CAL perspective (it may not)?

A: 

CAL does not support aliasing (which Haskell does with the 'type' keyword), so you can't just do:

type Patient = {name::String, age::Int}

However, you can create a newdata type that incorporates your record:

data Patient=
    public Patient
        details :: {name::String, age::Int}
    ;

... however, this is probably not what you need. Records as very handy for moving around bits of structured data and using a structural polymorphism (automatic projection of record subsets). You don't need to store the data like this though. Instead, I'd recommend:

data Patient=
    public Patient
        name :: !String
        age  :: !Int
    ;

The 'plings' on the types mean 'don't bother storing a lazy thunk here', e.g. we really want a string and and int even if you apply some complicated expression to the Patient constructor. You can safely omit the plings, but it's good practice to include them in most cases.

You can now use various forms of case analysis to extract elements from such a Patient value. You'll see all these in the manual, but here's a summary:

Overt case analysis, positional match:

age p =
    case p of
    Patientname age -> age;  // Spot the maintenance problem here!
    ;

Overt case analysis, symbol match:

nameAndAge p =
    case p of
    Patient{name,age} -> (name,age);  // Now it doesn't matter if the Patient constructor grows new arguments
    ;

Lazy extractor

name p =
    let
        Patient{name} = p;  // name is only extracted (and p partially evaluated) if name is required
    in
        name;

Single case extractor

name p =
    p.Patient.name;  // Syntactic sugar for example 1.  Useful when you are _only_ extracting a single field. 

You can always project a record from this data if you need to. Remember you can also have multiple constructors for the Patient data type, if there are several kinds of Patient.

For example, perhaps there are in-patients and out-patients. Both of these share some nhs patient records, but have specific fields pertinent to their treatment. We could represent along the following lines:

data Patient=
    public InPatient
        patientRecords :: !PatientRecord
        careCentreID   :: !Int
        department :: !String
        consultant :: !String
    | publicOutPatient
        patientRecords :: !PatientRecord
        appointments :: ![Appointment]
    ;


nhsRecords p =
    case p of
    (InPatient|OutPatient) {patientRecords} -> patientRecords;
    ;

This also allows us to look at a very powerful CAL feature that does multiple constructor matching. In this case we match InPatient and OutPatient, projecting only the patientRecords field.

This allows us to write an 'nhsRecords' extractor function that we can maintain fairly easily even as the details in the Patient constructors change.
Indeed, unless constructors come and go, or something happens to the "patientRecords" field itself, the this function need never change.

David Collie