[Haskell-cafe] Records in Haskell

Evan Laforge qdunkan at gmail.com
Thu Mar 1 22:33:02 CET 2012


> Thanks Evan, I've had a quick read through.

Thanks for reading and commenting!

> It's a bit difficult to compare to the other proposals.
>
> I can't see discussion of extracting higher-ranked functions and applying them
> in polymorphic contexts. (This is SPJ's `rev` example.)
>
> Putting h-r fields into records is the standard way of emulating object-
> oriented style. SPJ's view is that requirement is "very important in practice".
>
> (No proposal has a good answer to updating h-r's, which you do discuss.)

Yeah, I've never wanted that kind of thing.  I've written in
object-oriented languages so it's not just that I'm not used to the
feature so I don't feel its lack.  And if I did want it, I would
probably not mind falling back to the traditional record syntax,
though I can see how people might find that unsatisfying.  But my
suggestion is meant to solve only the problem of composed record
updates and redundant "thing"s in 'Thing.thing_field thing'.  Not
supporting higher-ranked function record fields *only* means that you
can't use this particular convenience to compose updates to a
higher-ranked field.  If you happen to have that particular
intersection of requirements then you'll have to fall back to typing
more "thing"s for that particular update.

My motivation is to solve an awkward thing about writing in haskell as
it is, not add a new programming style.

> Re the cons 1. "Still can't have two records with the same field name in the
> same module since it relies on modules for namespacing."
>
> Did you see the DORF precursor page ?
> http://hackage.haskell.org/trac/ghc/wiki/Records/DeclaredOverloadedRecordFields
> /NoMonoRecordFields
>
> I tried to figure out if that would help, but I suspect not. (Looking at the
> desugar for `deriving (Lens)`, you need the H98 field selector functions.)
> Then for me, cons 1. is a show-stopper. (I know you think the opposite.)

Yeah, I don't think the DORF precursor stuff is related, because it's
all based on typeclasses.  I think there are two places where people
get annoyed about name clashes.  One is where they really want to have
two records with the same field name defined in one module.  The other
is where they are using unqualified imports to shorten names and get a
clash from records in different modules.  Only the former is a
problem, the latter should work just fine with my proposal because ghc
lets you import clashing names as long as you don't call them
unqualified, and SDNR qualifies them for you.

So about the former... I've never had this problem, though the point
about circular imports forcing lots of things into the same module is
well taken, I have experienced that.  In that case: nested modules.
It's an orthogonal feature that can be implemented and enabled
separately, and can be useful in other ways too, and can be
implemented separately.  If we are to retain modules as *the* way to
organize namespaces and visibility then we should think about
fancying-up modules when a namespacing problem comes up.

Otherwise you're talking about putting more than one function into one
symbol, and that's typeclasses, and now you have to think of something
clever to counteract typeclasses' desire to be global (e.g. type
proxies).  Maybe that's forcing typeclasses too far beyond their
power/weight compromise design?

> I also don't see whether you can 'hide' or make abstract the representation of
> a record type, but still allow read-access to (some of) its fields.

If you want a read-only field, then don't export the lens for 'a',
export a normal function for it.  However, it would mean you'd use it
as a normal function, and couldn't pass it to 'get' because it's not a
lens, and couldn't be composed together with lenses.  I'd think it
would be possible to put 'get' and 'set' into different typeclasses
and give ReadLenses only the ReadLens dictionary.  But effectively
we'd need subtyping, so a Lens could be "casted" automatically to a
ReadLens.  I'm sure it's possible to encode with clever rank2 and
existentials and whatnot, but at that point I'm inclined to say it's
too complicated and not worth it.  Use plain functions.  Since 'get'
turns a lens into a plain function, you can still compose with
'#roField . get (#rwField1 . #rwField2)'.

We could easily support 'get (#roField1 . #roField2)' by doing the
ReadLens thing and putting (->) into ReadLens, it's just combining rw
fields and ro fields into the same composition that would require type
gymnastics.

> Suppose a
> malicious client declares a record with field #a. Can you stop them reading
> and/or updating your field #a whilst still letting them see field #b of your
> record type?

I don't think it's worth designing to support malicious clients, but
if you don't want to allow access to a function or lens or any value,
then don't export it.  #a can't resolve to M.a if M doesn't export
'a'.

> With SDNR, is it possibly to define a polymorphic field selector function? I
> suspect no looking at the desugar for `deriving (Lens)`, but perhaps I've mis-
> understood. I mean:
>    get_a r = ?? #a r     -- gets the #a field from any record r

Nope, 'r' has to be monomorphic.  That's the "no structural
polymorphism" thing I was talking about.  I don't mind not having it.
If we do that I believe we're moving to a fundamentally different type
of polymorphism, and, I think, a less principled one than typeclasses.

> This mechanism then supports the idea of 'virtual' fields -- SPJ's example of
> fullName, built from polymorphic firstName and lastName.

The read-only version is a plain function:

fullName r = firstName r ++ " " ++ lastName r

Because we are using plain functions we don't need to get all fancy
with "virtual" things, they're just normal functions.  If you want to
update fullName as well, then you could write a lens version:

fullName r = lens (firstName r ++ " " ++ lastName r)
  (\name -> let [first, last] = unwords name in set #firstName first .
set #lastName last)

Also, fclabels has a notion of views where you can express one type in
terms of another (say extract two fields as a pair) and then if you
update the first element of the pair it updates the record that came
from.  It seemed complicated, but maybe it has uses I haven't thought
of.

I suppose if you wanted to have a record with some internal and some
external ones, you could then define the external view as another
record, and then they can pass around the whole thing but only get to
fiddle with the external fields.  I would try to solve the problem
without views, e.g. nest the externally visible record inside the
internal one instead of trying to put all the fields into one record.

> [By the way, did you mean to post to the cafe only? Most of the discussion is
> going on on ghc-users.]

Oops, no I hadn't noticed which list the threads were on, thanks for
pointing it out.  Ccing ghc-users.

Anyway, one thing I think is different about this proposal is what I
said before: it aims to solve an awkward thing about haskell as it is,
not add a new programming style.  So it adds no power whatsoever.
It's more limited than the existing record notation, so it's not even
a replacement (but from what I understand, the other proposals aren't
either).  But it takes what is, for me, the 90% use case and
eliminates the redundant typing.  Maybe it's not nearly 90% for other
people, I can only speak for myself.



More information about the Haskell-Cafe mailing list