Changes between Version 1 and Version 2 of Records/DeclaredOverloadedRecordFields/OptionFourTypePunning

Mar 4, 2012 2:41:52 AM (6 years ago)



  • Records/DeclaredOverloadedRecordFields/OptionFourTypePunning

    v1 v2  
    11=== DORF `fieldLabel` Declaration through Type Punning ===
     3DORF's `filedLabel`'s generate a Proxy_type named for the label. Of course the `Proxy_` prefix to the name is arbitrary -- it just needs to be something to be easily desugarrable-to, and not clash with any other type name in the environment.
     5Despite all the frenzied comment on ghc-users, nobody's questioned the arbitrariness.
     6 * Why didn't I just name the type as per the label, but up-shifted?
     8Here's why: the ideas in DORF were triggered from this contribution by Chris Done (on the Records front page):
     10    Suppose I have 112 hand-crafted data types in my project (e.g. see attachment 51369.txt), this creates a lot of conflicts in field names and constructor names. For example:
     13data Comment = Comment {
     14      commentId           :: CommentId
     15    , commentContent      :: Content
     16    , commentReviewId     :: ReviewId
     17    , commentSubmissionId :: SubmissionId
     18    , commentConferenceId :: ConferenceId
     19    , commentDate         :: ISODate
     20    , commentReviewerNumber :: Int
     21  } deriving (Show)
     24    This is a real type in my project. ...
     26So for every field name, there's already a type with the same name (upshifted). And this is a standard, disciplined approach to building the naming structure for a large-scale database application.
     28In the sample, we haven't got the definition for type `CommentId`, so perhaps it's:
     30    type CommentId = Int
     32But that means we could do arithmetic on `CommentId`'s, and add them to `SubmissionId`'s then multiply by `ConfenceId` and put the result in `commentReviewerNumber`.
     34That sort of nonsense is exactly what SQL lets you do. (It even helpfully(?!) casts your numeric fields to the same format and lines up the decimal positions.) It sucks! It's exactly what strongly typed languages should stop you doing.
     36So more likely:
     38    newtype CommentId = CommentId Int
     40(Or perhaps it's a regular `data` type?)
     42So you've done that, then under DORF you need a `fieldLabel`, and then you can declare your record:
     44    fieldLabel commentId :: r -> CommentId
     46data Comment = Comment {
     47      commentId           :: CommentId
     48    , ...
     49    }
     52That's ''how many times'' I have to repeat the same name in one shift or the other? (Of course TH or a decent editor might reduce that, but it don't look pretty.) Plus there's a shadowy `Proxy_commentId` I have to worry about exporting/importing/hiding, so I can encapsulate my record properly.
     54Here's a radical suggestion:
     55 * don't declare your `newtype`
     56 * instead `fieldLabel` declares it for you (rather than generating a `Proxy_type`)
     57 * record declarations go like this:
     59data Comment = Comment {
     60      commentId                        -- no type declarations at all, at all
     61    , content                          -- } Note: no `comment` prefix to the field labels
     62    , reviewId                         -- }
     63    , ...
     64    , commentReviewerNumber :: Int     -- Not allowed! Must declare a fieldLabel/newtype ReviewerNumber.
     65    }                                  -- and put field name reviewerNumber
     67The data decl desugars to `Has` instances, using the upshifted field name as the type-level 'peg':
     69    instance (t ~ CommentId) => Has Comment CommentId t where ...
     72And you get a newtype `CommentId` to control the scope and representation -- which you're doing already.
     74Possible downsides:
     75 * Can't have two fields of the same type -- such as two `ISODate`'s in the example.[[BR]](Would have to be something like `submissionDate`, `conferenceDate`, etc. Applies particularly for 'generic' custom data types such as dates.)
     76 * We can't write polymorphic record types (with type vars), because there's no type declaration against the field name to hold it.[[BR]](Terrific! avoid all that complexity for updates that change the type of the record ;-)
     77 * Your `fieldLabel`'s (or `newtype`'s) can't have type arguments -- which is going to get increasingly irksome if your fields are in fact sub-records.
     78 * Or perhaps we allow type var(s) (or a type decl??) against the field name, but __very__ limited -- still need to validate against the `fieldLabel`/`newtype`.
     79 * Note that the effect of omitting a type decl for the field is different to H98: it __doesn't__ mean this field same type as the next; it means this field's type to be the upshifted name.
     80 * Perhaps to make that clear we use slightly different syntax: instead of comma separator between the fields, put semicolon.[[BR]]Makes sense: we're inside a curly bracketed scope.
     82So the `fieldLabel` and data decl would look like this:
     84fieldLabel commentId :: r -> Int
     85fieldLabel contentSource a :: (Contentful a) => r -> a
     87data Comment a = (Contentful a) => Comment {
     88      commentId
     89    ; content                          -- semicolon between the fields
     90    ; reviewId                     
     91    ; ...
     92    ; contentSource a                  -- typevar from the record parameter               
     93    ; ...
     94    ; commentReviewerNumber :: Int     -- Still not allowed!
     95    }     
     99For the implementors' benefit the desugar is:
     101newtype CommentId = CommentId Int                                 -- fieldLabel commentId
     102newtype ContentSource a = (Contentful a) => CommentSource a       -- contentSource
     104instance (Contenful a, t ~ ContentSource a, a ~ a_) =>
     105          Has (Comment a) (ContentSource a_) t           where ...
     107(Note the `a`, `a_` and type equality constraint: this is again using the functional-dependency-like mechanism.)