if
expressions
Table of contents
Overview
An if
expression is an expression of the form:
if
conditionthen
value1else
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 aB as CommonTypeWith(A)
implementation whenever one doesn’t exist but anA as CommonTypeWith(B)
implementation exists. CommonType
is defined in terms ofSymmetricCommonTypeWith
, and requires that bothA as SymmetricCommonTypeWith(B)
andB 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
- Provide no conditional expression
- Use
cond ? expr1 : expr2
, like in C and C++ syntax - Use
if (cond) expr1 else expr2
syntax - Use
if (cond) then expr1 else expr2
syntax - Allow
1 + if cond then expr1 else expr2
- Only require one
impl
to specify the common type if implicit conversions in both directions are possible - Introduce special rules for lvalue conditionals
References
- Proposal #911: Conditional expressions.