The basic idea

The Has and Upd classes, and GetResult and SetResult type families, are defined in the module ​GHC.Records in the base package.

Typechecking a record datatype still generates record selectors, but their names have a $sel prefix and end with the name of their type. Moreover, instances for the classes and type families are generated. For example,

The naming of cats

The AvailTC Name [Name] [(OccName, Name)] constructor of AvailInfo represents a type and its pieces that are in scope. Record fields are now stored in a separate list (the third argument), along with their selectors. The IEThingWith name [name] [OccName] constructor of IE, which represents a thing that can be imported or exported, only stores the field labels. SLPJ Whoa! Why should we duplicate this info. My gut feel is that the selector should not appear in the second argument. AMG Does this sound better now? It's helpful if gresFromAvail need not do lookups (it is called by the desugarer).

The Parent type has an extra constructor FldParent Name OccName that stores the parent Name and the field OccName. The GlobalRdrElt (GRE) for a field stores the selector name directly, and uses the FldParent constructor to store the field. Thus a field foo of type T gives rise this entry in the GlobalRdrEnv:

foo |-> GRE $sel_foo_T (FldParent T foo) LocalDef

SLPJ moreover I think we should store the dictionary$dfHasTfoo in the GRE for foo, not the selector. That way we get both getter and setter (via the dictionary) in one go. AMG Now I'm not sure about this. We can't build the dictionary for higher-rank fields, but they have a perfectly good selector. Moreover, with type-changing update there are two dictionaries (one for the getter and one for the setter) and two coercion axioms.

Note that the OccName used when adding a GRE to the environment (greOccName) now depends on the parent field: for FldParent it is the field label rather than the selector name.

The dcFields field of DataCon stores a list of FieldLabel, defined thus:

whereas the ifConFields field of IfaceConDecl stores a list of FieldLbl OccName.

Source expressions

The HsExpr type has extra constructors HsOverloadedRecFld OccName and HsSingleRecFld OccName id. When -XOverloadedRecordFields is enabled, and rnExpr encounters HsVar "x" where x refers to multiple GREs that are all record fields, it replaces it with HsOverloadedRecFld "x". When the typechecker sees HsOverloadedRecFld x it emits a wanted constraint Has alpha x beta and returns type alpha -> beta where alpha and beta are fresh unification variables.

When the flag is not enabled, rnExpr turns an unambiguous record field foo into HsSingleRecFld foo $sel_foo_T. The point of this constructor is so we can pretty-print the field name but store the selector name for typechecking.

Where an AST representation type (e.g. HsRecField or ConDeclField) contained an argument of type Located id for a field, it now stores a Located RdrName for the label, and some representation of the selector. The parser uses an error thunk for the selector; it is filled in by the renamer (by rnHsRecFields1 in RnPat, and rnField in RnTypes). The new definition of ConDeclField (used in types) is:

The renamer (rnHsRecFields1) supplies Left sel_name for the selector if it is unambiguous, or Right xs if it is ambiguous (because it is for a record update, and there are multiple fields with the correct label in scope). In the latter case, the possibilities xs are represented as a list of (parent name, selector name) pairs. The typechecker (tcExpr) tries three ways to disambiguate the update:

Perhaps only one type has all the fields that are being updated.

Use the type being pushed in, if it is already a TyConApp.

Use the type signature of the record expression, if it exists and is a TyConApp.

Automatic instance generation

Typeclass and family instances are generated, provided the extension is enabled, by makeLocalRecFldInsts and makeImportedRecFldInsts in TcInstDecls. These functions correspond to the two places in which instances are generated:

tcInstDecls1 generates instances for fields from datatypes in the current group (at the same time as derived instances, from deriving clauses, are generated)

tcRnImports in TcRnDriver generates instances for imported fields, by looking at the GlobalRdrEnv

The typeclass instances must be subsequently typechecked (by tcInstDecls2). Such instances are "private" in that they are available when typechecking the current module (in tcg_inst_env) but not exported to other modules (via tcg_insts). On the other hand, the underlying dfun ids, axioms and family instances are exported from the module as usual.

AMG This means that typeclass instances are not visible in GHCi, because it tracks only tcg_insts (via the InteractiveContext). What to do here? Perhaps a new field in TcGblEnv could record the private instances?

For each imported field, there are two possible cases:

If the module containing the field was compiled with -XOverloadedRecordFields, it will have the necessary dfuns already, so we can just look them up and add appropriate class instances to tcg_inst_env. Moreover, the family instances will have been imported.

Otherwise, all the instances must be generated, as if the field were locally defined.

Unused imports

Unused imports and generation of the minimal import list (RnNames.warnUnusedImportDecls) use a map from selector names to labels, in order to print fields correctly. However, fields may currently be reported as unused even if the corresponding Has instance is used. Consider the following:

Now, do we expect to report the 'x' in S(x) import as unused? Actually the entire 'import B' is unused. Only the typechecker will eventually know that. But I think the type checker does actually record which instances are used, so perhaps we can make use of that info to give accurate unused-import info.

AMG I thought this would be the tcg_ev_binds field of the TcGblEnv, but this seems to be empty by the end of tcRnModule. We could also look at the inert_solved_dicts field of InertSet, but I'm not sure how to propagate the required information out of the TcS monad to the TcM monad where unused names are reported.

The DEPRECATED pragma applies to all fields foo exported by the module M, since it is based on the OccName. The renamer will issue a deprecation warning for every use of foo in N, regardless of whether it will later resolve to one of the fields from M (as in goo), a field definitely not in M (as in bar), or a polymorphic field (as in baz). It might be possible to delay the warnings to type-checking time and report deprecations more precisely, as for unused imports.

GADT record updates

but this record update is rejected by the typechecker, even though it is perfectly sensible, because of #2595. The currently implemented workaround is instead to generate the explicit update

setField _ (MkW _ y) x = MkW x y

which is fine, but rather long-winded if there are many constructors or fields. Essentially this is doing the job of the desugarer for record updates.

Note that W does not admit type-changing single update for either field, because of the a ~ b constraint. Without it, though, type-changing update should be allowed.

Type-changing update: phantom arguments

Consider the datatype

data T a = MkT { foo :: Int }

where a is a phantom type argument (it does not occur in the type of foo). The traditional update syntax can change the phantom argument, for example if r :: T Int then r { foo = 3 } :: T Bool typechecks. However, setField cannot do so, because this is illegal:

type instance SetResult (T a) "foo" Int = T b

Note that the result of the type family involves an unbound variable b.

In general, a use of setField can only change type variables that occur in the field type being updated, and do not occur in any of the other fields' types.

However, what can we call the record selectors? They can't both be $sel_foo_F! Ideally we would use the name of the representation tycon, rather than the family tycon, but that isn't introduced until the typechecker (tcDataFamInstDecl in TcInstDcls), and we need to create the selector in the renamer (getLocalNonValBinders in RnNames). We can't just pick an arbitrary unique name, because we need to look up the selector to associate it with its data constructor (extendRecordFieldEnv in RnSource).

For the moment, I've simply disallowed duplicate fields for a single data family in a single module. It's fine to duplicate fields between different data families or across different modules, however.