Are anonymous type classes the right model at all? (replying to Re: Are fundeps the right model at all?)

Marcin 'Qrczak' Kowalczyk
30 Dec 2000 17:53:05 GMT

Fri, 29 Dec 2000 00:37:45 -0800, John Meacham <> pisze:


I've read it and posted some comments in February 2000. There was no
answer AFAIR. Here are they again, slightly edited and extended:

I don't understand why to separate kinds of rows and record types,
instead of having "a type which is known to be a record type", at
least on the level visible for the programmer. So instead of
    type Point        r = (r | x::Int, y::Int)
    type Colored      r = (r | c::Color)
    type ColoredPoint r = Point (Colored r)
    p :: {ColoredPoint()}
    -- Point, Colored, ColoredPoint :: row -> row
it would be
    type Point        r = {r | x::Int, y::Int}
    type Colored      r = {r | c::Color}
    type ColoredPoint r = Point (Colored r)
    p :: ColoredPoint()
    -- Point, Colored, ColoredPoint :: recordType -> recordType
    -- where recordType is something like a subkind of *.


It is bad to require the programmers to think in advance that a type
is going to be subtyped, and write elaborated
    type Point r = (r | x::Int, y::Int)
    ... {Point()} ...
instead of simpler
    type Point = {x::Int, y::Int}
    ... Point ...
which is not extensible.


I got used to () as a unit type. It would be a pity to lose it.


A minor problem. If tuples are records, field names should be such
that alphabetic order gives the sequential order of fields, or have
a special rule of field ordering for names of tuple fields...


In general I don't quite like the fact that records are getting more
anonymous. Magical instances of basic classes? How inelegant.

If I want the record type to have an identity, it will have to be
wrapped in a newtype, so I must think at the beginning if I will ever
want to write specialized insances for it and then all the code will
depend on the decision. Currently a datatype with named fields has
both an identity and convenient syntax of field access. (And why
newtype is not mentioned in section 5.1?)

I like name equivalence where it increases type safety. Extensible
records promote structural equivalence.

Unfortunately the proposal seems to increase the number of
irregularities and inelegant rules...

If expr.Constructor for a multiparameter constructor yields a tuple,
then for an unary constructor it should give a 1-tuple, no? I know
it would be extremely inconvenient, especially as newtypes are more
used, so I don't propose it, but it is getting less regular. What
about nullary constructors - empty tuple? :-)

I don't say that I don't like the proposal at all, or that I never
wanted to have several types with the same field names. But it is
not clean for me, it's a compromise between usability and elegance,
and from the elegance point of view I like current records more.

Maybe it would be helpful to show how to translate a program with
extensible records to a program without them (I guess it's possible
in a quite natural way, but requires global transformation of the
whole program).


Extensible records makes a syntactic difference between field access
and function call. So if one wants to export a type abstractly or
simply to provide functions operating on it without fixing the fact
that they are physically fields, he ends in writing functions like

size:: MyRecord -> Int
size x = x.MyRecord.size

which are unnecessary now, even if size is simply a field.

It reminds me of C++ which wants us to provide methods for accessing
data fields (for allowing them to be later redefined as methods,
and for allowing everything to be uniformly used with "()" after the
feature name). Ugh.


My new record scheme proposal does not provide such lightweight
extensibility, but fields can be added and deleted in a controlled
way if the right types and instances are made.

The distinction between having a field and having a supertype is
blurred. Similarly between having itself a field called foo and having
a supertype which has a field called foo. Similarly between creating
a record by adding fields to another record and creating a record by
putting another record as one of fields. Similarly between casting
to a supertype by removing some fields and extracting the supertype
represented by a field.

An advantage is that the interface of records does not constrain the
representation in any way. It's up to how instances are defined,
with the provision of natural definitions for records implemented
physically as product types.

For example supplying a color for a colorless point and the reverse
operation can be written thus:
    addColor :: (Record cp, cp.point :: p, cp.color :: Color)
             => p -> Color -> cp
    addColor p c = record point = p; color = c

    removeColor :: (cp.point :: p) => cp -> p
    removeColor = (.point)

When the following definitions are present:
    data Point = record
        x, y :: Int
    data ColoredPoint = record
        point :: Point
        point (x, y)
        color :: Color
these functions can be used as of types
    addColor    :: Point -> Color -> ColoredPoint
    removeColor :: ColoredPoint -> Point

A colored point can be constructed either as in addColor, from a
point and a color, or thus:
        x     = ...
        y     = ...
        color = ...

If ColoredPoint were defined directly as
    data ColoredPoint = record
        x, y  :: Int
        color :: Color
the previous interface could be *retroactively* reconstructed thus:
    instance (ColoredPoint).point :: Point where
        cp.point = record x = cp.x; y = cp.y
        cp.record {point = p} = cp.record x = p.x; y = p.y

Multiple inheritance can be modelled as well. And field renaming
during inheritance.

 __("<  Marcin Kowalczyk *
  ^^                      SYGNATURA ZASTĘPCZA