as expressionsWe would like to provide a notation for the following operations:
Requesting a type conversion in order to select an operation to perform, or to resolve an ambiguity between possible operations:
fn Ratio(a: i32, b: i32) -> f64 {
// Note that a / b would invoke a different / operation.
return a / (b as f64);
}
Specifying the type that an expression will have or will be converted into, for documentation purposes.
class Thing {
var id: i32;
}
fn PrintThing(t: Thing) {
// 'as i32' reminds the reader what type we're printing.
Print(t.id as i32);
}
Specifying the type that an expression is expected to have, potentially after implicit conversions, as a form of static assertion.
fn Munge() {
// I expect this expression to produce a Widget but I'm getting compiler
// errors and I'd like to narrow down why.
F(Some().Complex().Expression() as Widget);
}
In general, the developer wants to specify that an expression should be considered to produce a value of a particular type, and that type might be more general than the type of the expression, the same as the type of the expression, or perhaps might represent a different way of viewing the value.
The first of the above problems is especially important in Carbon due to the use of facet types for generics. Explicit conversions of types to interfaces will be necessary in order to select the meaning of operations, because the same member name on different facet types for the same underlying type will in general have different meanings.
For this proposal, the following are out of scope:
try_as or as? operations.C++ provides a collection of different kinds of casts and conversions from an
expression x to a type T:
T v = x;T v(x);static_cast<T>(x)const_cast<T>(x)reinterpret_cast<T>(x)dynamic_cast<T>(x)T(x) or equivalently (T)x
static_cast, const_cast, and
reinterpret_cast can do, but ignore access control on base classes.T{x}
T.These conversions are all different, and each of them has some surprising or unsafe behavior.
Swift provides four forms of type casting operation:
x as T performs a conversion from subtype to supertype.
pattern as T in a pattern matching context converts a pattern that
matches a subtype to a pattern that matches a supertype, by performing a
runtime type test. This effectively results in a checked supertype to
subtype conversion.x as! T performs a conversion from supertype to subtype, with the
assumption that the value inhabits the subtype.x as? T performs a conversion from supertype to subtype, producing an
Optional.T(x) and similar construction expressions are used to convert between
types without a subtyping relationship, such as between integer and
floating-point types.In Swift, x as T is always unsurprising and safe.
Rust provides the following:
x as T performs a conversion to type T.
This cast can perform some conversions with surprising results, such as integer truncation. It can also have surprising performance implications, because it defines the behavior of converting an out-of-range value -- for example, when converting from floating-point to integer -- in ways that aren't supported across all modern targets.
Haskell and Scala support type ascription notation, x : T. This has also been
proposed for Rust. This notation constrains the type checker to find a type for
the expression x that is consistent with T, and is used:
Carbon provides a binary as operator.
x as T performs an unsurprising and safe conversion from x to type T.
i32 to
f32.This operator does not perform conversions with domain restrictions, such as
converting from f32 to i64, where sufficiently large values can't be
converted. It does not perform operations in which there are multiple different
reasonable interpretations, such as converting from i64 to i32, where a
two's complement truncation might sometimes be reasonable but where the intent
is more likely that it is an error to convert a value that does not fit into an
i32.
See changes to the design for details.
as conversions, and encouraging
user-defined types to do the same, makes code easier to understand.as conversions and
potentially-unsafe conversions being performed by other syntax makes it
clearer which code should be the subject of more scrutiny when reasoning
about safety.As interface provides the same functionality as single-argument
explicit constructors and explicit conversion functions in C++, and
can be used to expose those operations for interoperability purposes and
as a replacement for those operations during migration.We need to provide additional conversions beyond those proposed for as. In
particular, to supply the same set of conversions as C++, we would need at least
the following conversions that don't match the rules for as:
Conversions with a domain restriction:
Conversions that modify some values:
Conversions that reinterpret values:
Special cases:
dynamic_cast.const_cast.We will need to decide which of these we wish to provide -- in particular,
depending on our plans for mutability and RTTI, const_cast and dynamic_cast
may or may not be appropriate.
For the operations we do supply, we could provide either named functions or
dedicated language syntax. While this proposal suggests that the as operator
should not be the appropriate language syntax for the above cases, that decision
should be revisited once we have more information from examining the
alternatives.
We could provide an additional casting operator, such as assume_as or
unsafe_as, to model conversions that have a domain restriction, such as
i64 -> i32 or f32 -> i64 or Base* -> Derived*.
Advantage:
Disadvantage:
If we don't follow this direction, we will need to provide these operations by another mechanism, such as named function calls.
as to perform some unsafe conversionsWe could provide a single type-casting operator that can perform some conversions that have a domain restriction, treating values out of range as programming errors.
One particularly appealing option would be to permit as to convert freely
between integer and floating-point types, but not permit it to convert from
supertype to subtype.
Advantage:
as more consistent with arithmetic operations, which will
likely have no overt indication that they're unsafe in the presence of
integer overflow.as, even if all conversions remain
in-bounds. If such code is common, as it is in C++ (for example, when mixing
int and size_t), developers may become accustomed to using that "assume
in range" notation and not consider it to be a warning sign, thereby eroding
the advantage of using a distinct notation.Disadvantage:
as and which cannot in general.as expression would be less suitable for selecting which operation to
perform if it can be unsafe.as would need additional scrutiny
because it's not in general a safe operation.The choice to not provide these operations with as is experimental, and should
be revisited when we have more information about the design of integer types and
their behavior.
as to perform two's complement truncationWe could allow as to convert between any two integer types, performing a two's
complement conversion between these types.
Advantage:
Disadvantage:
as conversions have behavior that diverges from the behavior of
arithmetic, where we expect at least signed overflow to be considered a
programming error rather than being guaranteed to wrap around.The choice to not provide these operations with as is experimental, and should
be revisited when we have more information about the design of integer types and
their behavior.
as only performs implicit conversionsWe could limit as to performing only implicit conversions. This would mean
that as cannot perform lossy conversions.
Advantage:
Disadvantage:
We could allow a conversion of integer types (and perhaps even floating-point
types) to bool, converting non-zero values to true and converting zeroes to
false.
Advantage:
Disadvantage:
as bool conversion is less clear to a reader than a != 0 test.as bool conversion is more verbose than a != 0 test.We could disallow conversions from bool to iN types.
Advantage:
bool as a truth value
rather than as a number.true should map to 1 (zero-extension)
or -1 (sign-extension).
Such conversions are a known source of bugs, especially when performed
implicitly. as conversions will likely be fairly common and routine in
Carbon code due to their use in generics. As such, they may be written
without much thought and not given much scrutiny in code review.
var found: bool = false;
var total_found: i32 = 0;
for (var (key: i32, value: i32) in list) {
if (key == expected) {
found = true;
total_found += value;
}
}
// Include an explicit `as i64` to emphasize that we're widening the
// total at this point.
// Bug: meant to pass `total_found` not `found` here.
add_to_total(found as i64);
Disadvantage:
b.AsBit() if we wanted.bool type and bits.We could disallow conversion from bool to i1.
Advantage:
true to -1
whereas all others convert true to 1.Disadvantage:
bool, and an awkward
special case that may get in the way of generics.bool that produces -1 for a true value is useful when
producing a mask, for example in (b as i1) as u32.