1 Add warning for incomplete record selectors

This proposal introduces a new warning flag, -Wincomplete-record-selectors, which emits a warning at use sites of partial selector functions or partial OverloadedRecordDot syntax.

1.1 Motivation

Haskell permits the definition of data types where a field occurs in some but not all constructors:

data T = T1 { x :: Int, y :: Bool }
       | T2 { y :: Bool }

The selector function x :: T -> Int is necessarily partial, and will throw an exception if called on a T2 value. Similarly, use of record dot syntax t.foo may throw an exception if t = T2.

Record updates such as t { x = 0 } are also partial, and there is an existing warning -Wincomplete-record-updates to warn about these.

The -Wpartial-fields warning gives one solution to this problem: rule out the definition of T, and require all fields to be total. However this is unnecessarily restrictive, because it rules out ever giving field names to heterogeneous multi-constructor datatypes, even though these are sometimes useful, and it is perfectly possible to use them without any partiality (by construction/pattern-matching, avoiding selection/update).

These warnings are intended to support a style of Haskell programming where uses of any partial functions are discouraged/minimized. This is a common (if not universal) approach. It is relatively easy to avoid uses of common partial functions such as head, because (a) programmers preferring this style learn which Prelude functions should be avoided anyway, and (b) explicit imports and/or a custom Prelude can render them out of scope or mark them with WARNING pragmas.

However, record fields are different: selector functions are generated by the compiler, regardless of imports, and some of these functions are partial. A codebase typically contains many domain-specific record datatype definitions and hence many fields, of which some selectors are partial but many are not. It thus becomes difficult to keep track of which fields are potentially partial, and indeed as code changes over time, previously total fields may become partial.

Moreover, while it is relatively easy for tooling to check for uses of well-known partial functions such as head, it becomes slightly harder to identify partial selector functions (since it requires analysis of many datatype definitions), and is yet harder still when HasField gets involved (since r.x may or may not refer to a partial selector x depending on the types). Having GHC do it would be easy.

See #7169, #17100, #18650 and discussion #459 for related requests.

1.2 Proposed Change Specification

A partial field is a field that does not belong to every constructor of the corresponding datatype.

A partial selector occurrence is a use of a record selector for a partial field, either as a selector function in an expression, or as the solution to a HasField constraint.

The new warning flag -Wincomplete-record-selectors will emit a warning for each partial selector occurrence for which the pattern match checker cannot statically determine that the selector is applied to a constructor that includes the field.

The exact capabilities of the pattern match checker are out of scope for this proposal. It is acceptable if all partial selector occurrences emit a warning, but the implementation may choose to suppress the warning in particular cases where it can determine that any argument to the partial selector will contain the field.

This warning is implied by -Wall, just like -Wincomplete-record-updates following proposal #71.

1.3 Examples

Recall the datatype from the Motivation:

data T = T1 { x :: Int, y :: Bool }
       | T2 { y :: Bool }

Here x is a partial field and y is a total field.

When -Wincomplete-record-selectors is enabled:

  1. An occurrence of x as a selector (in an expression) causes a warning. It is irrelevant whether or not it is applied. Thus f1 r = x r and g1 = x both warn, but h1 r = y r1 does not.

  2. A constraint HasField "x" T Int being solved automatically causes a warning.

    • In particular this arises with f2 = getField @"x" @T, but also with OverloadedRecordDot in cases such as g2 (r :: T) = r.x.

    • On the other hand h2 r = getField @"x" r and k2 r = r.x do not warn because their types are polymorphic in the record type, subject to a HasField constraint.

    • A later call to h2 or k2 at type T does trigger a warning, because this leads to the constraint HasField "x" T Int being solved.

  3. Uses of the field x in record construction or pattern-matching do not lead to a warning, so these are fine:

    h3 = T1 { x = 3, y = True }
    
    k3 T1{x=x'} = x'
    k3 T2{} = 0
    

1.3.1 Long range information

Expressions such as the following will obviously never cause a pattern match failure at runtime, because x is applied to an argument that will necessarily use the T1 constructor:

x (T1 { x = 0, y = True })

case r of { T2 _ -> 0 ; _ -> x r }

let t1 = T1 { x = 0, y = True } in t1.x

Thus the implementation may be able to suppress the warning, depending on the capabilities of the pattern match coverage checker.

1.3.2 GADTs

Consider the following GADT:

data G a where
  MkG1 :: { x :: Int    } -> G Bool
  MkG2 :: { y :: Double } -> G Char

Any use of x or getField @"x" applied to a term of type G a will result in a warning. However if the argument type is G Bool then the warning may optionally be suppressed, for example, this definition need not emit a warning:

f :: G Bool -> Int
f r = getField @"x" r

1.4 Effect and Interactions

The NoFieldSelectors extension allows users to suppress field selector functions, thereby avoiding the risk of calling a partial selector function in an expression. This does not prevent use of OverloadedRecordDot for the field, however, so the proposed warning is still useful.

This proposal assumes that HasField constraints always represent selectors, not updates. This is true in currently implemented GHC versions, but would no longer be true if proposal #158 was to be implemented as currently specified. I intend to bring forward a separate proposal to split updates into a separate class, thereby avoiding this issue (see also proposal #286).

This proposal makes no changes to -Wpartial-fields, so that users may choose to receive warnings at definition sites or at use sites. Both may be useful in different contexts:

  • a library author may wish to enable -Wpartial-fields to avoid ever defining a partial field in their library, since they have no guarantee that downstream users will enable the use-site warnings;

  • an application author may be using an existing library that defines partial fields, but may wish to avoid using them by enabling -Wincomplete-record-selectors -Wincomplete-record-updates.

1.5 Costs and Drawbacks

The implementation cost of this warning should be low, as GHC can easily determine which fields are partial, and record this information for later use.

Users who set -Wall -Werror may see build failures if they use partial fields as selectors, but if this is not desired they can set -Wno-incomplete-record-selectors.

1.6 Alternatives

For HasField, it would be possible to change its definition so that it would not be solved at all for partial fields, or provide an alternative implementation (either manually or automatically) returning a Maybe value. This would avoid partiality when using OverloadedRecordDot, without a need for warnings. It seems simplest to keep HasField consistent with existing selector functions, however.

This does not make it possible for a library author to define a datatype with partial fields such that their users cannot use partial operations. Instead, downstream modules will need to enable -Werror=incomplete-record-selectors in order to rule out such cases. We could imagine somehow annotating datatypes to impose restrictions such as preventing selection or update, but this is not part of the current proposal.

1.6.1 Naming

The new flag is named -Wincomplete-record-selectors for consistency with the existing -Wincomplete-record-updates (and similarly-named warnings such as -Wincomplete-patterns). These all share the property of warning about code that necessarily performs an incomplete pattern match.

The naming of -Wpartial-fields at first seems inconsistent with this, and we might imagine changing it to something like -Wincomplete-record-definitions. However, it is somewhat different to the others, because it is possible to define a partial field but use it only through total mechanisms (e.g. pattern matching). If we were to define a warning group -Wincomplete to collect together incompleteness warnings (as suggested in discussion on proposal 351) it would make sense to include -Wincomplete-record-selectors and -Wincomplete-record-updates but not -Wpartial-fields. Thus this proposal does not change the name of -Wpartial-fields.

1.7 Unresolved Questions

None.