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.

  • -XStrictData changes 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 LazyFieldAnnotations enabled and StrictData disabled, an unannotated field remains lazy. A field written ~ty has the same semantics as an unannotated field ty.

  • With StrictData enabled, behaviour is unchanged: unannotated fields are strict. Because StrictData implies LazyFieldAnnotations, ~ty is also accepted and marks that field lazy.

  • With StrictData enabled and LazyFieldAnnotations explicitly disabled by a later NoLazyFieldAnnotations, unannotated fields remain strict, but ~ty is 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 requiring StrictData.

  • 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 StrictData imply LazyFieldAnnotations;

  • 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.