proposals/p007254-replace-and-with-keywords-and-contextual-defaults.md
:! and :? with keywords and contextual defaultsThis proposal removes the :! syntax for generics and templates in favor of
keywords (generic, template, runtime) and contextual defaults for phase.
It also suggests replacing :? from proposal #5389 with fwd and renaming
"forms" to "extended types".
The :! syntax for generics and templates has several issues:
! is tenuous and not an
effective mnemonic.!, such as for
operations that are required to succeed or terminate (for example,
unwrapping optionals).The :! syntax was originally chosen to evoke the idea of "phase", using ! to
mark parameters that belong to an earlier (compile-time) phase of evaluation.
Similarly, the :? syntax in the current revision of proposal #5389 is intended
to mark deduced form bindings: parameters that capture extended type
information (what was called a "form") about an expression, such as its value
category and phase, rather than just its object type.
These issues were discussed in leads issue #6932, and a direction was decided to move away from punctuation and towards keywords and contextual defaults.
We propose to:
:! syntax for generics and templates.template, generic, and
runtime.exttype. Also suggest replacing the :? and ->? syntax from
pending proposal #5389 with a binding modifier fwd and corresponding
return syntax.Parameter phase is primarily determined by the context of the parameter:
These defaults can be overridden where meaningful by using one of the following keywords:
runtime: Causes a parameter to be a runtime parameter in the deduced
parameter context, if we ever decide to support runtime deduced parameters.generic: Causes a parameter to be a checked generic when in an explicit
function parameter context.template: Causes a parameter to be a template generic in any of the three
contexts.Compile-time entities: Parameters to entities like interface, impl,
and class are checked generic parameters by default.
interface I(T: type) { ... } // T is a checked generic parameter
They can be marked as template:
class C(template T: type) { ... } // T is a template generic parameter
Deduced function parameters: Parameters in [] for functions default to
checked generic parameters.
fn F[T: type](arg: T); // T is a checked generic parameter
They can be marked as template:
fn F[template T: type](arg: T); // T is a template generic parameter
If we ever add deduced runtime parameters (anticipated for scoped parameters
like allocators), they would be marked with the runtime keyword:
fn F[runtime heap: Heap](T: type, arg: T) -> T*; // heap is a runtime parameter
Explicit function parameters: Parameters in () for functions default
to runtime parameters.
fn F(arg: i32); // arg is a runtime parameter
They can be marked as generic or template:
fn F(generic T: type, arg: T); // T is a checked generic parameter
Keywords are only allowed where needed to override the contextual default. This avoids confusion and ensures consistency.
The checked generic default for deduced parameters applies only to declared
parameters in the [] list. Lambda captures, which also appear in [] but are
syntactically distinguished (they are not declared names), are not affected by
this default. Instead, a capture retains the phase of the expression being
captured, which we expect to be important for the usability of lambdas.
Associated constants in interfaces require no extra keywords. Their meaning is guided by the context of the interface definition itself.
The conceptual model is that an interface is essentially a class whose phase is
inherently the symbolic compile-time (generic) phase. As a consequence, its
fields (the associated constants) act as generic constants naturally, and
placing an additional phase keyword on them would be redundant and disallowed.
The same logic applies when implementing those constants in an impl, which
already uses distinct syntax to assign them.
This proposal replaces the concept of "forms" (as described in
/docs/design/values.md) with extended types.
The term "forms" was originally used to generalize types to include expression
category, phase, and value. However, this terminology was found to conflict
confusingly with the concept of "unformed state". To resolve this, we move to a
model where these are considered "extended types", connecting them more directly
to the type system while preserving type for standard object types.
Under this new design:
form(expr) is renamed to exttype(expr).Core.ExtType (replacing Core.Form).:? syntax for deduced form bindings is suggested to be
replaced by a binding modifier fwd. This modifier causes the
right-hand-side of the binding's : to be converted to Core.ExtType
rather than type. This is a suggested (but not fully decided) direction
for pending proposal #5389 to go with the syntax.fwd is also suggested for use in the return signature (for example,
-> fwd T) to forward the extended type information. Note that this may end
up being more significant than just a syntactic replacement: it remains to
be decided in proposal #5389 whether fwd must appear directly after the
-> (matching how ->? works in that proposal currently) or if it can be
used within tuple syntax in the return type, similar to how ref is
allowed.This approach allows us to reclaim high-value punctuation like ? for other
uses (such as optional types) while providing a more explicit and less
punctuated syntax for advanced generic programming.
Example:
fn F[T: Core.ExtType](fwd arg: T) -> fwd T;
Open question: Should we require the
fwdmodifier on call arguments as well, analogously to howrefis required on arguments for reference parameters?
Once the design for extended types in proposal #5389 is more complete, we may
also want to replace Core.ExtType with a new built-in keyword exttype for
the type of extended types, and potentially replace exttype(expr) literals
with library entities. This would make exttype analogous to type in the
grammar.
This proposal advances the following Carbon goals and principles:
Code that is easy to read, understand, and write: Removing dense punctuation in favor of keywords with meaningful names makes code less like ASCII-art and more immediately readable. The contextual defaults are carefully chosen to match what nearly all code uses in practice, keeping keywords sparse while remaining explicit when they are needed.
Software and language evolution:
Reclaiming ! and ? as punctuation opens up syntax space for other
high-value features. In particular, ! is a strong candidate for operations
that are required to succeed or terminate (for example, unwrapping
optionals), which would have been visually ambiguous if ! were also used
for generics.
Progressive disclosure: The contextual defaults allow learners to work with generic interfaces and classes without needing to understand or type phase keywords at first. Keywords only become relevant when departing from the defaults, which is a rarer, more advanced case. This mirrors how Carbon teaches other concepts progressively.
Prefer only one way to do a given thing:
Disallowing redundant phase keywords (those that match the contextual
default) ensures there is exactly one canonical way to write each parameter
declaration, consistent with how Carbon handles other defaults such as
public access.
:! syntaxOne alternative was to retain the existing punctuation-based syntax where :!
is used to denote checked generic parameters and template generic parameters.
[] and
() lists does not change its meaning. Under the proposed contextual
defaults, such a move changes the default phase and requires adding a
keyword to preserve it, making this kind of refactoring of a function
signature slightly less straightforward.! and generics is not an effective mnemonic.!, such as for operations that are
required to succeed or terminate (for example, unwrapping optionals).Several alternative keywords were considered for the three phase keywords.
For generic, the key candidates considered were:
symbolic: Reflects the technical description of symbolic compile-time
evaluation.comptime: Reflects when the value is known (compile time).checked: Reflects the semantic behavior that these parameters are
type-checked at the definition site.For runtime, the main candidate discussed was:
dynamic: Reflects that values are dynamically determined at runtime.Looking across these options:
symbolic is more technically precise for compiler experts as it
reflects the symbolic evaluation phase.comptime is a familiar pattern from other modern systems languages.checked is highly precise about the checking model used, matches the
terminology we use in the design, and matches the structure of
template.dynamic uses a term that is recognizable for runtime behavior.symbolic is inaccessible jargon and less teachable to developers not
familiar with compiler or type theory terminology.comptime describes when the value is known, not why or how it is
used, lacking a connection to generic programming.checked focuses on the implementation mechanism (checking) rather than
the programmer's intent (generic programming) and loses the immediate
familiarity of the term generic.dynamic conflicts with the well-established use in dynamic dispatch
(for example, Rust's dyn), making it a poor fit for Carbon.generic, template, runtime) were
found to best balance accessibility with precision. generic in particular
connects directly to the well-known concept of generic programming, making
it both familiar and teachable.template generic instead of just templateAn alternative considered was to require template generic (two keywords) for
template generic parameters, and generic for checked generic parameters, to
make it clear that templates are generics.
Under this model, the terminology is that we have "generic parameters" that come
in two semantic forms: "checked generic parameters" and "template generic
parameters". Both of these are considered "generic parameters". The default
semantic is checked generic parameters, so when a parameter is marked generic
(or defaults to it), it gets that semantic. The rejected alternative would be to
use both keywords as template generic for the template case, rather than
omitting the generic keyword and just using template.
template
keyword on a parameter is for it to be a generic parameter, so adding
generic provides no additional information.template to avoid
unnecessary verbosity.An alternative approach proposed making the phase of every parameter fully
explicit in its declaration, without any contextual defaults. The specific
proposal from the discussion used a static modifier for compile-time value
parameters, so that the phase could always be read directly from the declaration
without needing to know whether the parameter appears in () or []:
fn MakeArray(T: type, static Length: i32) -> Array(T, Length);
fn ReverseArray[T: type, static Length: i32](ref a: Array(T, Length));
This is analogous to how ref and val modifiers make value categories
explicit today, with the goal of making each parameter declaration
self-contained.
static is heavily overloaded in C++, covering storage duration, class
membership, and file-scope linkage, which creates significant confusion
for C++ developers migrating to Carbon.static
keyword for compile-time integers creates pressure towards having
separate compile-time and runtime vocabulary types, which Carbon has
aimed to avoid to keep the type vocabulary compact.static keyword in particular was found
to have significant overloading concerns coming from C++.An alternative approach proposed using type erasure as the foundational mental
model for generic parameters, paralleling the way languages like Java implement
generics. Under this model, a generic type parameter is said to be "erased" at
runtime: the type information is available at compile time but not preserved in
the runtime representation. This would use erased as the keyword instead of
generic:
// T is erased (available at compile time, erased at runtime)
interface I(T: type) {
fn Op(self, arg: T) -> T;
}
// Explicit erased parameter in a function
fn ScopedParams[runtime heap: Heap](erased T: type) -> T*;
This model has particular implications for associated constants in interfaces. Under the erased model, associated constants would be thought of as values that are erased from the runtime representation (present at compile time but not available at runtime), rather than as fields of a compile-time class that are inherently generic by context.
generic more directly connects
to the reason a developer reaches for this feature.generic versus template terminology split, which is the clearest
way to distinguish the two distinct kinds of compile-time parameters in
Carbon, is obscured by "erased" framing, since templates are not erased.generic/template split and the
interface-as-compile-time-class model. The team preferred a terminology that
describes the programming concept rather than an implementation detail,
and found the model where interface fields are inherently generic by context
to be more intuitive and consistent with the rest of the design.One alternative suggested was to make explicit function parameters default to
checked generic if they cannot be represented at runtime (such as types). This
would allow omitting generic even in the explicit () parameter list when the
parameter type makes the phase unambiguous:
fn F1[Q: type](arg1: Q, QQ: type, arg2: QQ) -> (Q, QQ);
T: type always implies compile-time,
regardless of position.generic for
compile-time integers but not for compile-time types creates an
inconsistent rule that would be difficult to learn.:! provided today.Another alternative was to allow keywords matching the contextual default to be
used optionally, for example allowing generic T: type in a deduced parameter
list where checked generic is already the default semantic.
public in a context where public is already
the default access, for the same reason: explicit statement of a default
implies it was chosen deliberately, which is misleading.exprtype and expr keywordsOne alternative considered for replacing "forms" was to use the terminology
"expression types" with exprtype as the bottom type and expr as the binding
modifier.
type as the primary term
for object types and qualifying it as exprtype for expression types.expr on a binding that the expression itself is
bound and captured, rather than being evaluated first. Hard to explain
that this matches the evaluated expression.Core.ExtType). For the binding modifier, fwd was chosen
because it connects to the use case of forwarding extended type information
(similar to C++ std::forward) and fits well as a three-letter keyword
similar to ref, var, and val.