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:
An occurrence of
x
as a selector (in an expression) causes a warning. It is irrelevant whether or not it is applied. Thusf1 r = x r
andg1 = x
both warn, buth1 r = y r1
does not.A constraint
HasField "x" T Int
being solved automatically causes a warning.In particular this arises with
f2 = getField @"x" @T
, but also withOverloadedRecordDot
in cases such asg2 (r :: T) = r.x
.On the other hand
h2 r = getField @"x" r
andk2 r = r.x
do not warn because their types are polymorphic in the record type, subject to aHasField
constraint.A later call to
h2
ork2
at typeT
does trigger a warning, because this leads to the constraintHasField "x" T Int
being solved.
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.