1 Lazy Field Annotations¶
This proposal introduces -XLazyFieldAnnotations. The extension allows the
existing prefix ~ field annotation syntax in data and GADT constructor
fields without having to enable -XStrictData. -XStrictData would imply
-XLazyFieldAnnotations, while continuing to control the default strictness
of unannotated fields. This allows users and source generators to write
laziness explicitly without also changing that default.
See discussion in GHC issue #24455.
1.1 Motivation¶
GHC already supports two ways to talk about constructor-field strictness in surface syntax:
Prefix
!explicitly marks a field strict.-XStrictDatachanges the default so that unannotated fields are strict, and prefix~can be used to opt a field back to lazy.
This coupling is awkward. A programmer or code generator may want to express that a field is lazy explicitly, without changing the meaning of every other unannotated field in the module.
For example, this declaration is accepted in any module:
data A = A !Int
But the analogous lazy annotation is rejected unless StrictData (or
Strict, which implies it) is enabled:
data B = B ~Int
Today GHC reports that “Lazy field annotations (~) are disabled” and suggests
enabling StrictData. But enabling StrictData is not just a syntactic
change: it also changes the default semantics of every unannotated field in the
module.
This particularly affects generated declarations. Template Haskell already has
SourceLazy in Language.Haskell.TH.Syntax, but using it without
StrictData is rejected for the same reason. As a result, generators must
inspect the ambient extension set and emit either an explicit ~ annotation
or a plain field type depending on whether StrictData is enabled. Similar
issues arise for other source generators such as happy; see this comment
on haskell/happy issue #273.
This proposal unbundles “allow the explicit ~ syntax” from “change the
module-wide default for unannotated fields”. It addresses a small but real gap
in the language design of StrictData: syntax and semantics are currently
bundled together.
This proposal is deliberately narrow. It is not the proposal in GHC issue
#16836 to require every
field to be annotated with either ! or ~. It only adds the missing
opt-in syntax for explicit laziness.
1.2 Proposed Change Specification¶
A new language extension LazyFieldAnnotations is added. The extension is
disabled by default.
StrictData implies LazyFieldAnnotations. Therefore Strict also
implies LazyFieldAnnotations transitively.
As with other implied extensions, NoLazyFieldAnnotations can override this
implication when it appears later in the extension list, whether in a
LANGUAGE pragma or on the command line.
Prefix ~ is accepted as a lazy field annotation in every constructor-field
position where prefix ! is accepted today if and only if
LazyFieldAnnotations is enabled.
More precisely, this proposal reuses the existing syntax and semantics of lazy
field annotations under StrictData. The syntax is controlled solely by
LazyFieldAnnotations; StrictData only affects the default meaning of
unannotated fields, while implying LazyFieldAnnotations.
1.2.1 Syntax¶
This applies to:
Haskell-98 prefix constructor fields
Haskell-98 record fields
GADT constructor argument types
GADT record fields
For example, all of the following declarations are accepted when
LazyFieldAnnotations is enabled:
data A = A ~Int Bool
data B = B { b1 :: ~Int, b2 :: !Bool }
data C where
C1 :: ~Int -> C
C2 :: { c1 :: ~Int, c2 :: !Bool } -> C
In the terminology introduced by proposal #402,
the grammar already admits a strictness_sigil of either ! or ~.
Today that proposal specifies the side condition:
In
strictness_sigil, the~is guarded behind-XStrictData.
This proposal changes that side condition to:
In
strictness_sigil, the~is guarded behind-XLazyFieldAnnotations.
The corresponding Haskell-98 constructor-field syntax is changed in the same
way: wherever a field strictness annotation is currently permitted, a prefix
~ is accepted when LazyFieldAnnotations is enabled.
1.2.2 Semantics¶
LazyFieldAnnotations controls whether explicit ~ syntax is accepted.
It does not change the default strictness of any unannotated field.
With
LazyFieldAnnotationsenabled andStrictDatadisabled, an unannotated field remains lazy. A field written~tyhas the same semantics as an unannotated fieldty.With
StrictDataenabled, behaviour is unchanged: unannotated fields are strict. BecauseStrictDataimpliesLazyFieldAnnotations,~tyis also accepted and marks that field lazy.With
StrictDataenabled andLazyFieldAnnotationsexplicitly disabled by a laterNoLazyFieldAnnotations, unannotated fields remain strict, but~tyis rejected.
LazyFieldAnnotations has no effect outside constructor-field types. In
particular, it does not change the syntax or meaning of term-level irrefutable
patterns.
This proposal does not relax the existing restriction on newtype
constructors: newtype fields still must not have a strictness annotation.
For example, this declaration remains invalid:
newtype N = N ~Int
Existing rules for {-# UNPACK #-} and {-# NOUNPACK #-} are unchanged.
{-# UNPACK #-} only takes effect on a strict field, so it has nothing to
unpack on a field marked lazy with ~. As today, GHC ignores UNPACK on
such a field and emits its usual warning; this proposal merely makes the ~
annotation writable without StrictData, and does not change how these
pragmas behave.
1.3 Proposed Library Change Specification¶
None.
Template Haskell already exposes the existing SourceLazy annotation. This
proposal does not add new library API; it only changes when that existing
annotation is accepted in generated declarations.
1.4 Examples¶
With LazyFieldAnnotations alone, the default remains lazy:
{-# LANGUAGE LazyFieldAnnotations #-}
data A = A ~Int Bool
Both fields of A are lazy. The ~ on the first field is explicit but
semantically redundant.
With StrictData, which implies LazyFieldAnnotations, the default
remains the one from StrictData:
{-# LANGUAGE StrictData #-}
data B = B ~Int Bool
The first field of B is lazy and the second is strict.
The implied extension can still be disabled explicitly by listing
NoLazyFieldAnnotations later:
{-# LANGUAGE StrictData, NoLazyFieldAnnotations #-}
data C = C Int Bool
Both fields of C are strict, and writing ~Int in that module would be
rejected.
The extension also applies to record and GADT syntax:
{-# LANGUAGE GADTs, LazyFieldAnnotations #-}
data T where
MkT :: { lazyField :: ~Int, strictField :: !Bool } -> T
Generated code can now say what it means directly instead of branching on
StrictData. For example, a Template Haskell declaration using
SourceLazy can be accepted in a module that enables
LazyFieldAnnotations even if it does not enable StrictData.
1.5 Effect and Interactions¶
This proposal addresses the motivating use cases directly:
Template Haskell code can generate explicit lazy fields in modules that enable
LazyFieldAnnotations, without requiringStrictData.Other code generators, such as
happy, can emit explicit lazy field annotations uniformly instead of adapting their output to the ambient default.Hand-written code gains a way to document that a field is intentionally lazy, symmetric with today’s explicit
!syntax.
Interaction with StrictData is intentionally conservative: existing
StrictData and Strict code is unchanged.
The proposal does not solve every strictness-annotation issue. In particular,
newtype constructors still reject strictness annotations, so generators that
produce both data and newtype declarations still need to account for
that distinction.
No new warning classes are introduced. Existing warnings, such as the warning
that {-# UNPACK #-} on a lazy field lacks a !, are unchanged.
At the same time, making both ! and ~ available independently of the
module default may make a future warning such as
-Wimplicit-field-strictness more practical, by letting users write either
choice explicitly without also opting into StrictData. Such a warning is
outside the scope of this proposal.
1.6 Costs and Drawbacks¶
The implementation cost is small, but not zero:
GHC gains one more language extension to document, test, and maintain.
Users need to learn another extension name.
Outside
StrictData, the annotation~is semantically redundant, so some users may regard it as visual noise.
The proposal also does not address the broader desire for mandatory strictness/laziness annotations on all fields; that remains a separate design question.
1.7 Backward Compatibility¶
This proposal has expected impact level 0 under the scale in the proposal template: no breakage.
No existing program changes meaning. With LazyFieldAnnotations enabled,
strictly more programs are accepted. Existing StrictData code is unchanged.
There is no migration burden. Users and code generators may opt into the new extension when they want to write lazy field annotations explicitly.
1.8 Alternatives¶
1.8.1 Keep the status quo¶
Code generators can continue to inspect whether StrictData is enabled and
either emit ~ty or erase the annotation. This is workable, but it pushes
extension-dependent bookkeeping into every generator and does not help
hand-written code.
1.8.2 Accept ~ everywhere, perhaps with a warning¶
One alternative is to make lazy field annotations accepted unconditionally, and
perhaps warn when ~ is redundant in a lazy-by-default module. This would
avoid a new extension name, but it is a broader language change. The proposal
here keeps the change explicitly opt-in and therefore aligns better with GHC’s
stability goals.
1.8.3 Silently erase SourceLazy outside StrictData¶
Template Haskell helper functions could try to compensate for the missing
surface syntax by dropping SourceLazy when StrictData is disabled. This
is insufficient:
it only helps some Template Haskell APIs;
it does not help hand-written source code;
it does not help non-TH generators such as
happy;it makes generated code depend on ambient extension settings in a less direct and less visible way.
1.8.4 Add local modifiers for StrictData¶
Another alternative is to add syntax for locally enabling or disabling
StrictData around a declaration, so that the default strictness is stated
near the datatype rather than at module level. This would be useful beyond this
proposal, and resembles previous discussions about local control of extensions
or compiler flags.
However, local extension modifiers are significantly more complicated to specify and implement. They would also control the default for unannotated fields rather than directly addressing the missing ability to write an explicit lazy annotation in lazy-by-default code.
1.8.5 Require every field to be annotated¶
GHC issue #16836
suggests a broader extension that would require each field to carry either
! or ~. That is a different proposal. The present proposal only adds
the missing syntax needed to express explicit laziness independently of
StrictData.
1.9 Unresolved Questions¶
None.
1.10 Implementation Plan¶
No implementation commitment is required for the proposal to stand. The expected implementation work is modest:
adjust the existing extension guard for lazy field annotations;
make
StrictDataimplyLazyFieldAnnotations;add parser and renamer/typechecker tests for H98, record, and GADT syntax;
update the Users’ Guide documentation for constructor-field strictness.
1.11 Acknowledgments¶
Thanks to Oleg Grenrus for raising GHC issue #24455, and to the commenters on that issue and GHC issue #16836 for discussion.