Polymorphic record update

Henrik Nilsson nhn at Cs.Nott.AC.UK
Mon Jan 23 15:52:12 EST 2006


Hi,

Einar Karttunen wrote:

 > Here is a proposal to allow polymorphic record update for the existing
 > record system. I think it makes sense if the current record system is
 > kept.
 >
 > Given a record like:
 >
 > data Foo a = Foo { bar :: a }
 >
 > it would be nice to be able to update it like:
 >
 > f = Foo { bar = 'a' }
 > g = f { bar = False }
 >
 > constructing the new record by hand is quite tedious for records
 > with many fields and one ends up writing helper functions to
 > do this.

The above actually does work as far as I can tell (and GHCi agrees)?

However, it is realated to a problem with the current record system that
has annoyed me for years, and I suspect that is what Einar is referring
to. If the current record system essentially is kept for Haskell', it
would definitely be worth considering fixing that problem.

I started writing up a proposal a while ago, but never quite got
around to finalizing it. It's enclosed, but thus does have some
rough edges and is lacking some detail. But I hope it explains
the problem and outlines a reasonable solution.

If it does not get shot down immediately, I'll put it on the Wiki.
I think there is a page for tweaks to the record system there.

Best,

/Henrik

-- 
Henrik Nilsson
School of Computer Science and Information Technology
The University of Nottingham
nhn at cs.nott.ac.uk

This message has been checked for viruses but the contents of an attachment
may still contain software viruses, which could damage your computer system:
you are advised to perform your own checks. Email communications with the
University of Nottingham may be monitored as permitted by UK legislation.

-------------- next part --------------
Polymorphic Record Update
-------------------------

Consider the following data type:

    data T a
      = C1 { f1 :: a }
      | C2 { f1 :: a, f2 :: Int }
      | C3 { f2 :: Int }
      deriving Show

Suppose we want to update the field "f1" only in such a way that
its type changes. We cannot use the record update syntax, as not
all constructors have a field "f1". So we write a utility function.
However, we would prefer to do as little as possible when it
comes to values constructed by constructors NOT having a field
"f2". One might naively try this:

    foo :: T a -> T Int
    foo x@(C1 {}) = x {f1 = 1}
    foo x@(C2 {}) = x {f1 = 2}
    foo x         = x

But of course, this does not type check as the type of "x" is
different on the LHS and RHS. We can get around that by reconstructing
the value on the RHS:

    foo :: T a -> T Int
    foo x@(C1 {})       = x {f1 = 1}
    foo x@(C2 {})       = x {f1 = 2}
    foo x@(C3 {f2 = n}) = C3 {f2 = n}

However, this is bad, because we have to change the code if further
constructors are added, even when they do not have a field "f1",
and we also have to change the code if further fields are added
to constructors not having the field "f1". This is tedious,
error prone, and really defeats one of the main reasons for using
records in the first place. For example:

    data T a
      = C1 { f1 :: a }
      | C2 { f1 :: a, f2 :: Int }
      | C3 { f2 :: Int, f3 :: Char }
      | C4 { f2 :: Int }
      deriving Show

    foo :: T a -> T Int
    foo x@(C1 {})               = x {f1 = 1}
    foo x@(C2 {})               = x {f1 = 2}
    foo x@(C3 {f2 = n, f3 = c}) = C3 {f2 = n, f3 = c}
    foo x@(C4 {f2 = n})         = C4 {f2 = n}

One might think it would be possible to do better if we're furtunate
enough to have a field that is common to *all* constructors not having
a field "f1", as is the case for "f2" in this case: 

    foo :: T a -> T Int
    foo x@(C1 {}) = x {f1 = 1}
    foo x@(C2 {}) = x {f1 = 2}
    foo x         = x {f2 = f2 x}

But this does not type check, and it would not apply anyway if
there is no such common field.

What we really need is a function that reconstructs a value of type "T a"
at type "T b" for all values constructed by a constructor that does not have
a field "f1":

    coerce_no_f1 :: T a -> T b
    coerce_no_f1 x@(C3 {f2 = n, f3 = c}) = C3 {f2 = n, f3 = c}
    coerce_no_f1 x@(C4 {f2 = n})         = C4 {f2 = n}
    
    foo :: T a -> T Int
    foo x@(C1 {}) = x {f1 = 1}
    foo x@(C2 {}) = x {f1 = 2}
    foo x         = coerce_no_f1 x

But we'd rather not have to write such functions by hand, just as
we'd rather not write update functions by hand. Maybe the record
update syntax could be extended so that the function that gets
generated behind the scenes only includes constructors that
does NOT mention a particular field. For example, the field
name(s) that must not occur could be prefixed by "~" which suggests
negation in some settings. It does not have this connotation in Haskell,
but at least "~" is already a special symbol. We could then write:

    foo :: T a -> T Int
    foo x@(C1 {}) = x {f1 = 1}
    foo x@(C2 {}) = x {f1 = 2}
    foo x         = x {~f1}

Now the code for "foo" only has to be changed if new constructors
having a field "f1" are added.

Of course, it should be possible to combine this with the normal
record update syntax. E.g.

    foo :: T a -> T Int
    foo x@(C1 {}) = x {f1 = 1}
    foo x@(C2 {}) = x {f1 = 2}
    foo x         = x {~f1, f2 = f2 x + 1}



More information about the Haskell-prime mailing list