# if expressions
# Table of contents
# Overview
An if expression is an expression of the form:
ifconditionthenvalue1elsevalue2
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
SymmetricCommonTypeWithinterface implicitly provides aB as CommonTypeWith(A)implementation whenever one doesn't exist but anA as CommonTypeWith(B)implementation exists. CommonTypeis 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.