I have been thinking about records in Gofer, Haskell, and MHDL (Yet
Another Haskell Related Language) for a little while.  I know this is
a little late in the game for Haskell 1.3 (and so on), but this is the
first moment I have had to explore this a little, and I did want to
post my thoughts.

All this was started by a message from Simon Peyton-Jones on way back
on May 16, 1994 (yes, that long ago).  In it, he proposes using the
type system to construct, by a disciplined set of declarations
(possibly generated by some syntax sugar), a scheme for extensible
records.  I was very much attracted to the idea, as it seemed to
handle some of the name space problems that Nick North objected to as
far ago as November of 1993.  Very nice.

In his followup, Mark Jones suggested that we make a class for each
field in any record, and extend records by making an instance for the
class for any record type that does *not* have a field of that name.
This had some attraction; however, it also required changing all the
declarations each time you declare a new record, which is OK as a
basic language feature but not as a very cheap add-on to an existing
language.  However, the idea of a class for each field seemed to have
some interesting possibilities, so I decided to explore them.

First, I want to define some things we should be able to do with any
record and any field:

> class FieldName a f b where
>       set:: a -> f -> b -> a
>       get:: a -> f -> b

Set does just what you think it would do: sets the value for a field
of type "b" in a record of type "a" (ignore the f for the moment).
Similarly, get does what is intuitively obvious: gets a value of type
"b" from a record of type "a".  Of course, if we are going to have get
and set routines of this generality, we need to specify the field as
well.  This is the reason for the "f" type above: that type specifies
the field.  Yes, a type for each field.  It does work out nicely when
you try it.

Let's do a record like Date (day: Int, month: Int, year: Int).  Let's
ignore the type of the implementation of the record for a moment, and
do the class definitions for the fields and the record itself.

> data Day = Day       deriving Text
> data Month = Month   deriving Text
> data Year = Year     deriving Text
>
> class FieldName a Day b => DayField a b where
>    day:: a -> b
>    day d = get d Day
>
> class FieldName a Month b => MonthField a b where
>    month:: a -> b
>    month d = get d Month
>
> class FieldName a Year b => YearField a b where
>    year:: a -> b
>    year d = get d Year

As you can see, I am not above using the initial case rule for types
versus function names to my advantage.  No, not at all.  Not one
little bit.

Now for the record declaration:

>
> class (DayField a Int, MonthField a Int, YearField a Int) => Date a

We actually have a little more freedom here if we wish.  We can give
Date more parameters, and have those parameters reflected in the
context if desired.  This was not necessary here, so I did not bother;
however, the possiblity should be evident to all.

The implementation, given a type, is so basic that the instances
virtually write themselves:

>
> data DateRec = DateRec Int Int Int
>
> instance FieldName DateRec Day Int where
>          set (DateRec _ a b) Day x = DateRec x a b
>          get (DateRec x _ _) Day = x
>
> instance FieldName DateRec Month Int where
>          set (DateRec a _ b) Month x = DateRec a x b
>          get (DateRec _ x _) Month = x
>
> instance FieldName DateRec Year Int where
>          set (DateRec a b _) Year x = DateRec a b x
>          get (DateRec _ _ x) Year = x
>
> instance Date DateRec

As with Simon's proposal, we have complete freedom over which
implementation we choose to use.  A tuple would have done just as
well.  I have chosen to use the simple field names for the type and
the function names rather than the class name (where I append a Name
suffix).  This is a matter of taste; however, I anticipate using the
names in the get and set routines more often than declaring records
with them.

If we find that we need to worry about BC as well as AC, we can extend
the date accordingly:

> data AncientMark = AncientMark  deriving Text
>
> class FieldName a AncientMark b => AncientMarkField a b where
>       ancientMark:: a -> b
>       ancientMark r = get r AncientMark
>
> data BC_AD = BC | AD
>
> class (Date a, AncientMarkField a BC_AD) => AncientDate a

I omit the (fairly obvious) example of an implementation here.  Again,
the mark could have been anything that we wished, even polymorphic (if
we wish to make the record class polymorphic in that parameter as
well).

Now, we move on to features that are more unique to this approach of
defining field names as records.  We an also define record for a
person giving his name and the year of his birth.  The field name for
the year we already have defined; we simply add a field name for the
name of the person involved.

> data Name = Name  deriving Text
> class FieldName a Name b => NameField a b where
>    name:: a -> b
>    name d = get d Name
>
> class (YearField a Int, NameField a String) => Student a

Here, just for the heck of it, we give an implementation to prove that
we can indeed have two different implementations of two different
records with the same field name.  Just for fun, let's implement this
as a tuple rather than with a constructor.

>
> type StudentRec = (String, Int)
>
> instance FieldName StudentRec Name String where
>          set (_, a) Name x = (x, a)
>          get (x, _) Name = x
>
> instance FieldName StudentRec Year Int where
>          set (a, _) Year x = (a, x)
>          get (_, x) Year = x
>
> instance Student StudentRec

And year now is a field for both Dates and Students.  Suppose, now, we
want to augment our student record with a full date, rather than just
a year.  Well, the inheritence for the year field name should work
through both classes, so this should be a simple matter of combining
them.

> class (Date a, Student a) => SpecificStudent a

And we are done.  Records with the same field name of different types
will fail on the implementation; the instance of the class for the
record will be ambiguous.

Using single value data types to mark the field names has more
benefits than just resolving ambiguity in the get and set relations.
It makes field names first class values in virtually every sense.  For
example, we can extract any field from a record and return a string
that has the name of the record and its value:

> getval:: (FieldName a f b, Text f, Text b) => a -> f -> (String, b)
> getval r n = ((show n) ++ ": " ++ (show t), t) where t = get r n

Just try doing *that* in Pascal!  Thus, we can operate on two numeric
fields of any record if we wish.  I don't know how useful such a
facility is; however, it is at least interesting.

Comments and corrections are, of course, most welcome.

                                        Dave Barton <*>
                                        [EMAIL PROTECTED] )0(
                                        http://www.inmet.com/~dlb/


Reply via email to