Variable type inference
Table of contents
- Problem
- Background
- Proposal
- Open questions
- Rationale based on Carbon’s goals
- Alternatives considered
Problem
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.
Background
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.
Variable type inference in other languages
#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.
Carbon equivalents
In Carbon, we have generally discussed:
var x: auto = 5;
However, given precedent from other languages, we could omit this:
var x = 5;
Pattern matching
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): ... }
Carbon equivalents
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.
Pattern matching on if
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
match
itself; it may be worth considering whether the feature is sufficiently distinct and valuable to support.
Proposal
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.
Open questions
Inferring a variable type from literals
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.
Rationale based on Carbon’s goals
- 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
auto
work similarly to C++’s type inference, in order to ease migration.
- The intent is to have
Alternatives considered
Elide the type instead of using auto
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
auto
is 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
auto
in 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.
Use _
instead of auto
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
.