Variable type inference

Pull request

Table of contents

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

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