Add setField to HasField

This is a proposal to adjust the built-in typeclass HasField, removing getField and adding hasField (which is powerful enough to get, set and modify a field). The result would allow type-based resolution of field names in functions that update records. It does not introduce any new extensions.

Motivation

A serious limitation of the Haskell record system is the inability to overload field names in record types: for example, if the data types

data Person  = Person  { personId :: Int, name :: String }
data Address = Address { personId :: Int, address :: String }

are in scope in the same module, there is no way to determine which type an occurrence of the personId record selector refers to. The HasField extension defined in the already-implemented Overloaded Record Fields proposal introduced HasField, to allow type-based resolution of field names and polymorphism over record selectors. The class HasField is currently defined as:

-- | Constraint representing the fact that the field @x@ belongs to
-- the record type @r@ and has field type @a@.
class HasField (x :: k) r a | x r -> a where
  getField :: r -> a

While this class provides a way to get a field, it provides no way to set a field. To quote the previous proposal:

In the interests of simplicity, this proposal does not include a class to provide polymorphism over record updates

Such a proposal to deal with record updates would clearly be desirable.

Proposed Change Specification

We propose to adjust HasField in GHC.Records to become:

-- | Constraint representing the fact that the field @x@ can be get and set on
--   the record type @r@ and has field type @a@.  This constraint will be solved
--   automatically, but manual instances may be provided as well.
--
--   The function should satisfy the invariant:
--
-- > uncurry ($) (hasField @x r) == r
class HasField x r a | x r -> a where
  -- | Function to get and set a field in a record.
  hasField :: r -> (a -> r, a)

We propose to have GHC automatically solve new HasField constraints the same way it does for the existing HasField constraints.

To enhance reverse compatibility and make it easier to use the hasField function, we propose also adding to GHC.Records:

getField :: forall x r a . HasField x r a => r -> a
getField = snd . hasField @x

setField :: forall x r a . HasField x r a => r -> a -> r
setField = fst . hasField @x

This proposal does not change how record updates are desugared.

Effect and Interactions

Using hasField it is possible to write a function:

mkLens :: forall x r a . HasField x r a => Lens' r a
mkLens f r = wrap <$> f v
    where (wrap, v) = hasField @x r

And thus allow generating lenses from the field classes. The function setField is also useful in its own right, complementing the existing getField method and providing the ability to modify records by field name.

Costs and Drawbacks

More code in the compiler.

Alternatives

Separate getField and setField methods

An alternative is to provide two separate methods, rather than the combined hasField. The separate methods are both simpler, but to implement any fields that perform computation (e.g. delving into a Map) would require performing that computation twice in a field modification. By combining the two functions that extra cost can be eliminated.

Separate methods would also avoid breaking compatibility for people who have already defined HasField. However, a search of Hackage has not identified anyone defining HasField, so the breakage is minor.

Polymorphic updates

A type-changing update is one where the type r is higher-kinded and the field x is the only member of that type. As an example, given a value of type (Int, Bool), the selector pointing to the first component, and a new value of type Double we can produce (Double, Bool). The design space for type-changing updates is large, and almost certainly requires additional complexity. In contrast, the design space for type-preserving updates is small and it can be easily incorporated into the existing design. The addition of type-preserving updates in no way constrains the design space for future type-changing updates, but is useful in its own right.

Read-only fields

By splitting the type class we could support read-only fields. However, read-only fields are essentially just functions, and we already have good support for functions throughout Haskell. In addition, it would likely be necessary to have a decision procedure for whether a field was read-only, which would quickly become unweildy.

Unresolved Questions

None.

Implementation Plan

Adam Gundry has offered to implement this feature.