- Problem
- Background
- Proposal
- Open questions
- Rationale based on Carbon's goals
- Alternatives considered
Inferring variable types is a common, and desirable, use-case. In C++, the use
of auto for this purpose is prevalent. We desire this enough for Carbon that
we already make prevalent use in example code.
Although #826: Function return type inference introduced the auto
keyword for fn, the use type inference in var should be expected to be more
prevalent.
In C++, auto can be used in variables as in:
auto x = DoSomething();In Carbon, we're already using auto extensively in examples in a similar
fashion. However, it's notable that in C++ the use of auto replaces the actual
type, and is likely there as a matter of backwards compatibility.
#618: var ordering chose the ordering of var based on other languages. Most of these also provide inferred variable types.
Where the <identifier>: <type> syntax matches, here are a few example inferred
types:
Ada appears to require a variable type in declarations.
For different syntax, Go switches from
var x int = 5 to x := 5, using the := to trigger type inference.
In Carbon, we have generally discussed:
var x: auto = 5;However, given precedent from other languages, we could omit this:
var x = 5;As we consider variable type inference, we need to consider how pattern matching would be affected.
Swift's patterns support:
-
Value-binding patterns, as in:
switch point { case let (x, y): ... }
-
Expression patterns, as in:
switch point { case (0, 0): ... case (x, y): ... }
-
Combining the above, as in:
switch point { case (x, let y): ... }
With Carbon, we have generally discussed:
-
Value-binding patterns, as in:
match (point) { case (x: auto, y: auto): ... }
-
Expression patterns, as in:
match (point) { case (0, 0): ... case (x, y): ... }
-
Combining the above, as in:
match (point) { case (x, y: auto): ... }
However, it may be possible to mirror Swift's syntax more closely; for example:
-
Value-binding patterns, as in:
match (point) { case let (x, y): ... }
-
Expression patterns, as in:
match (point) { case (0, 0): ... case (x, y): ... }
-
Combining the above, as in:
match (point) { case (x, let y): ... }
In the above, this presumes to allow let inside a tuple in order to indicate
the variable-binding for one member of a tuple.
In Carbon, it's been suggested that if could support testing pattern matching
when there's only one case, for example with auto:
match (point) {
case (x, y: auto):
// Pattern match succeeded, `y` is initialized.
default:
// Pattern match failed.
}
=>
if ((x, y: auto) = point) {
// Pattern match succeeded, `y` is initialized.
} else {
// Pattern match failed.
}A let approach may still look like:
match (point) {
case (x, let y):
// Pattern match succeeded, `y` is initialized.
default:
// Pattern match failed.
}
=>
if ((x, let y) = point) {
// Pattern match succeeded, `y` is initialized.
} else {
// Pattern match failed.
}Some caveats should be considered for pattern matching tests inside if:
-
There is syntax overlap with C++, which allows code such as:
if (Derived* derived = dynamic_cast<Derived*>(base)) { // dynamic_cast succeeded, `derived` is initialized. } else { // dynamic_cast failed. }
-
Syntax is similar to
matchitself; it may be worth considering whether the feature is sufficiently distinct and valuable to support.
Carbon should offer variable type inference using auto in place of the type,
as in:
var x: auto = DoSomething();At present, variable type inference will simply use the type on the right side.
In particular, this means that in the case of var y: auto = 1, the type of y
is IntLiteral(1) rather than i64 or similar. This
may change, but is the simplest
answer for now.
Using the type on the right side for var y: auto = 1 currently results in a
constant IntLiteral value, whereas most languages would suggest a variable
integer value. This is something that will be considered as part of type
inference in general, because it also affects generics, templates, lambdas, and
return types.
- Code that is easy to read, understand, and write
- Frequently code will have the type on the line, as in
var x: auto = CreateGeneric[Type](args). Avoiding repetition of the type will reduce the amount of code that must be read, and should allow readers to comprehend more quickly.
- Frequently code will have the type on the line, as in
- Interoperability with and migration from existing C++ code
- The intent is to have
autowork similarly to C++'s type inference, in order to ease migration.
- The intent is to have
As discussed in background, other languages allow eliding the type. Carbon could do similar, allowing:
var y = 1;Advantages:
- Forms a cross-language syntax consistency with languages that put the type
on the right side of the identifier.
- This is particularly strong with Kotlin and Swift, because they also use
var.
- This is particularly strong with Kotlin and Swift, because they also use
- Use of
autois currently unique to C++; Carbon will be extending its use.
Disadvantages:
- Type inference becomes the lack of a type, rather than the presence of something different.
- C++'s syntax legacy may have needed
autoin order to provide type inference, where Carbon does not. - Removes syntactic distinction between binding of a new name and reference to
an existing name, particularly for pattern matching.
Reintroducing this distinction would likely require additional syntax, such
as Swift's nested
let.
We expect there will be long-term pushback over the cross-language
inconsistency, particularly as the var <identifier>: auto syntax form will be
unique to Carbon. However, there's a strong desire not to have the lack of a
type mean the type will be inferred.
We have discussed using _ as a placeholder to indicate that an identifier
would be unused, as in:
fn IgnoreArgs(_: i32) {
// Code that doesn't need the argument.
}We could use _ instead of auto, leading to:
var x: _ = 1;However, removing auto entirely would also require using _ when inferring
function return types. For example:
fn InferReturnType() -> _ {
return 3;
}Advantages:
- Reduces the number of keywords in Carbon.
- Less to type.
- The incremental convenience may ameliorate the decision to require a keyword for type inference.
Disadvantages:
- There's a feeling that
_means "discard", which doesn't match the semantics of inferring a return type, since the type is not discarded. - There may be some ambiguities in handling, such as
var c: (_, _) = (a, b). - The reduction of typing is unlikely to address arguments that the keyword is not technically required.
The sense of "discard" versus "infer" semantics is why we are using auto.