Useless use of @as

@as in Zig doesn't cast. If you get an error about incompatible types, @as will do absolutely nothing to help you.

const my_float: f64 = 5.5;
const my_int: u64 = my_float;
_ = my_int;
// error: fractional component prevents float value '5.5' from coercion to type 'u64'

Let's add @as and see if it helps!

const my_float: f64 = 5.5;
const my_int: u64 = @as(u64, my_float);
_ = my_int;
// error: fractional component prevents float value '5.5' from coercion to type 'u64'

Wow, it did nothing!

If we want this to work, we have to use the intFromFloat built-in.

const my_float: f64 = 5.5;
const my_int: u64 = @as(u64, @intFromFloat(my_float));
_ = my_int;
// It compiles!

Whoa! It compiles. But there's another problem.

This program has introduced a useless-use of @as. See, we added @intFromFloat but didn't remove the @as. This is a common mistake people make when first debugging Zig code. They need to do a simple cast, they try using @as, and then when it doesn't do anything, they leave it in. We can get rid of the @as and our program still compiles.

const my_float: f64 = 5.5;
const my_int: u64 = @intFromFloat(my_float);
_ = my_int;
// No error.

And this isn't specific to f64 and u64. This is true for any two types. If you get an error about incompatible types without @as, you're going to get an error with @as.

Is @as useless?

If @as doesn't cast, is it useless? No. To explain what @as does though, you need some understanding of RLS. (This is a very rough sketch of the RLS concept and in particular, result-types.)

RLS stands for Result Location Semantics. This means that (in Zig) an expression can have a Result Type.

Think of the Result Type as a property of the hole that you're going to fill with some value. In the following code, three examples of "holes" are denoted with ....

const a: usize = ...;
const b: usize = 5 + a + ... + 3;

const Side = enum { left, right };
extern myFunction(foo: Side) void;
myFunction(...);

Can you determine what the result types of these "holes" are?

Zig is smart, and so it can deduce the type of two of these result locations, but not the type of the third. Let's take them one at a time.

Example 1

const a: usize = ...;

The Result Type here is obviously usize, because that type is annotated as part of the variable declaration. The compiler knows that regarless of the type of the value or expression used here. If the expression has a different type than `usize`, then Type Coercion is performed.

https://ziglang.org/documentation/master/#Type-Coercion Type Coercion in the Zig Language Reference

Example 2

const b: usize = 5 + a + ... + 3;

In this example, the type of the "hole" is unknown.

Result Type information is optional. Most of the time, this hole will be filled with an expression that has a known type (either another comptime_int or another usize variable).

const b: usize = 5 + a + 2 + 3;

b and a are usize. 5, 2, 3 are comptime_int, and Zig can do peer-type resolution on these types, so there's no issue.

Example 3

Before getting into an example where the type of the expression is also unknown, let's look at our third example. This is the other example where Zig can determine the type of the result.

const Side = enum { left, right };
extern myFunction(foo: Side) void;
myFunction(...);

myFunction is going to be called with an instance of the Side enum, since that's the type declared in its function signature.

RLS is what allows you to write code like myFunction(.left) and have Zig know that .left is referring to the type of Side, and no some other enum with a .left feild.

What if there is no Result Type

Let's say we wanted to write code like this:

const b: usize = 5 + a + @intFromFloat(5.5) + 3;

What's the type of the expression @intFromFloat(5.5)?  @intFromFloat returns anytype. Let's look at the documentation:

Converts the integer part of a floating point number to the inferred result type.

Okay. This is the same result type we were talking about before. Zig uses this result type to determine the result integer type of @intFromFloat.

But wait. This is the second example again, where there was no result type. What does the compiler do?

const b: usize = 5 + a + @intFromFloat(5.5) + 3;
// error: @intFromFloat must have a known result type
// note: use @as to provide explicit result type

Aha! The compiler errors, and the hint? "use @as"

The real use of @as

This is why @as exists in the language. @as allows you to create a "hole" with a defined result type (without needing an intermediate variable or function call).

Our code above can be fixed by adding @as and specifying the Result Type.

const b: usize = 5 + a + @as(usize, @intFromFloat(5.5)) + 3;
// Compiles!

There are definite uses for this, like when doing math with floats and ints in a single expression. But @as is never the thing doing the casting. All it's doing is telling Zig what the Result Type of a cast should be.

How to actually cast

Zig doesn't have a single syntax for casting.

Instead it has a number of built-ins which cast between different types with different semantics. When casting from a float to an int:

Ultimately, these builtins give fine-grained control over casting which is less error prone and more useful than a single casting builtin. But man, wouldn't it be nice if @as actually did everything for you!

The title of this post is modeled off of "useless use of cat."
Background from Hero Patterns; CC BY 4.0