if expressions

Table of contents

Overview

An if expression is an expression of the form:

if condition then value1 else value2

The condition is converted to a bool value in the same way as the condition of an if statement.

Note: These conversions have not yet been decided.

The value1 and value2 are implicitly converted to their common type, which is the type of the if expression.

Syntax

if expressions have very low precedence, and cannot appear as the operand of any operator, except as the right-hand operand in an assignment. They can appear in other context where an expression is permitted, such as within parentheses, as the operand of a return statement, as an initializer, or in a comma-separated list such as a function call.

The value1 and value2 expressions are arbitrary expressions, and can themselves be if expressions. value2 extends as far to the right as possible. An if expression can be parenthesized if the intent is for value2 to end earlier.

// OK, same as `if cond then (1 + 1) else (2 + (4 * 6))`
var a: i32 = if cond then 1 + 1 else 2 + 4 * 6;

// OK
var b: i32 = (if cond then 1 + 1 else 2) + 4 * 6;

An if keyword at the start of a statement is always interpreted as an if statement, never as an if expression, even if it is followed eventually by a then keyword.

Semantics

The converted condition is evaluated. If it evaluates to true, then the converted value1 is evaluated and its value is the result of the expression. Otherwise, the converted value2 is evaluated and its value is the result of the expression.

Finding a common type

The common type of two types T and U is (T as CommonType(U)).Result, where CommonType is the Carbon.CommonType constraint. CommonType is notionally defined as follows:

constraint CommonType(U:! CommonTypeWith(Self)) {
  extend CommonTypeWith(U) where .Result == U.Result;
}

The actual definition is a bit more complex than this, as described in symmetry.

The interface CommonTypeWith is used to customize the behavior of CommonType:

interface CommonTypeWith(U:! type) {
  let Result:! type
    where Self impls ImplicitAs(.Self) and
          U impls ImplicitAs(.Self);
}

The implementation A as CommonTypeWith(B) specifies the type that A would like to result from unifying A and B as its Result.

Note: It is required that both types implicitly convert to the common type. Some blanket impl declaractions for CommonTypeWith are provided as part of the prelude. These are described in the following sections.

Note: The same mechanism is expected to eventually be used to compute common types in other circumstances.

Symmetry

The common type of T and U should always be the same as the common type of U and T. This is enforced in two steps:

  • A SymmetricCommonTypeWith interface implicitly provides a B as CommonTypeWith(A) implementation whenever one doesn’t exist but an A as CommonTypeWith(B) implementation exists.
  • CommonType is defined in terms of SymmetricCommonTypeWith, and requires that both A as SymmetricCommonTypeWith(B) and B as SymmetricCommonTypeWith(A) produce the same type.

The interface SymmetricCommonTypeWith is an implementation detail of the CommonType constraint. It is defined and implemented as follows:

interface SymmetricCommonTypeWith(U:! type) {
  let Result:! type
    where Self impls ImplicitAs(.Self) and
          U impls ImplicitAs(.Self);
}
match_first {
  impl forall [T:! type, U:! CommonTypeWith(T)]
      T as SymmetricCommonTypeWith(U) where .Result = U.Result {}
  impl forall [U:! type, T:! CommonTypeWith(U)]
      T as SymmetricCommonTypeWith(U) where .Result = T.Result {}
}

The SymmetricCommonTypeWith interface is not exported, so users may not declare their own implementations of it, and only the two blanket impl declarations above are used. The CommonType constraint is then defined as follows:

constraint CommonType(U:! SymmetricCommonTypeWith(Self)) {
  extend SymmetricCommonTypeWith(U) where .Result == U.Result;
}

When computing the common type of T and U, if only one of the types provides a CommonTypeWith implementation, that determines the common type. If both types provide a CommonTypeWith implementation and their Result types are the same, that determines the common type. Otherwise, if both types provide implementations but their Result types differ, there is no common type, and the CommonType constraint is not met. For example, given:

// Implementation #1
impl forall [T:! type] MyX as CommonTypeWith(T) where .Result = MyX {}

// Implementation #2
impl forall [T:! type] MyY as CommonTypeWith(T) where .Result = MyY {}

MyX as CommonTypeWith(MyY) will select #1, and MyY as CommonTypeWith(MyX) will select #2, but the constraints on MyX as CommonType(MyY) will not be met because result types differ.

Same type

If T is the same type as U, the result is that type:

final impl forall [T:! type] T as CommonTypeWith(T) where .Result = T {}

Note: This rule is intended to be considered more specialized than the other rules in this document.

Because this impl is declared final, T.(CommonType(T)).Result is always assumed to be T, even in contexts where T involves a symbolic binding and so the result would normally be an unknown type whose facet type is type.

fn F[T:! Hashable](c: bool, x: T, y: T) -> HashCode {
  // OK, type of `if` expression is `T`.
  return (if c then x else y).Hash();
}

Implicit conversions

If T implicitly converts to U, the common type is U:

impl forall [T:! type, U:! ImplicitAs(T)]
    T as CommonTypeWith(U) where .Result = T {}

Note: If an implicit conversion is possible in both directions, and no more specific implementation exists, the constraints on T as CommonType(U) will not be met because (T as CommonTypeWith(U)).Result and (U as CommonTypeWith(T)).Result will differ. In order to define a common type for such a case, CommonTypeWith implementations in both directions must be provided to override the blanket impl declarations in both directions:

impl MyString as CommonTypeWith(YourString) where .Result = MyString {}
impl YourString as CommonTypeWith(MyString) where .Result = MyString {}
var my_string: MyString;
var your_string: YourString;
// The type of `also_my_string` is `MyString`.
var also_my_string: auto = if cond then my_string else your_string;

Alternatives considered

References