# Pre-RFC: Add explicitly-named numeric conversion APIs

I can get behind the `NaN` and `+/-inf` returning an error. Even quite explicit ones,

``````enum RoundError {
Nan,
InfinityPositive,
InfinityNegative
}

impl f64 {
type Output = u32;

fn try_round_to<Output>(self) -> Result<Output, RoundError>;
}
``````

Or something similar (since this RFC doesn't recommend traits, though I think that it should)

I agree that `as` should be replaced by more explicit methods. Below are just nits.

For combinations of primitive number types where they are implemented, the general-purpose convert::Into and convert::From traits offer exact conversion: o always represents the exact same real number as i.

Floating-point types are not subsets of the real number +/- Infinity and NaN. Also, there is -0. Is converting -0f32 to f64 preserve -0? Is it can be 0? Is a bit pattern of NaN preserved?

The `I::modulo_to<O>(self) -> O` method for modulo conversion, also known as bit-truncating conversion.

I find this naming confusing as `modulo_to` seems like a binary operation. I propose it `I::bit_truncate_to`.

Write that this method is only implemented if and only if the number of bits of `O` is less than the number of bits of `I`.

The `I::saturating_to<O>(self) -> O` method for saturating conversion. `o` is the value arithmetically closest to `i` that `O` can represent. This is `O::MIN` or `O::MAX` for underflow or overflow respectively.

The condition of the method is the same as above.

"Arithmetically close" is not a usual term and just "close" is enough I think. Also, the term "underflow" has a different meaning when used for floating-point numbers. Alternative terms are "negative overflow" and "positive overflow".

Rounding is according to roundTiesToEven mode as defined in IEEE 754-2008 §4.3.1: pick the nearest floating point number, preferring the one with an even least significant digit if exactly halfway between two floating point numbers.

In my opinion, the rounding mode should be more explicit. I propose to name it `I::round_ties_to_even` and preferably provide other rounding modes.

I’d rather not go into the merits of mapping `NaN` to zero. Please consider that part as a placeholder for “whatever `as` will do after https://github.com/rust-lang/rust/issues/10184 is fixed”. If you’d prefer `as` to have some other behavior, please propose it in that thread.

It’s quite possible that I’m using the word modulo incorrectly. I see that we already have APIs like `wrapping_add`, so `wrapping_to` is probably a better name.

Regarding `to` v.s. `into` I’ll refer to API naming guidelines. The `Into` trait has this name because it is general-purpose and also used with some input types that are `!Copy`. Primitive number types however are `Copy`, and taking `self` by value does not transfer ownership.

Would `approximate_to` or `approx_to` sound better than `round_to`? “Truncate” seems incorrect in that case since the approximation is to the nearest, not towards zero.

The standard library already makes it a non-goal to support code generic over number types. Before 1.0 it had `Int` and `Float` traits that were moved to the `num` crate. I feel changing that should come before supporting conversions in generic-number code. And it’s stuff for another RFC.

I also (and this is for the unwritten Prior Art section) wanted to depart from the `FromLossy` / `TryFromLossy` RFC which proposes general-purpose traits. IMO a big problem of that RFC is that it’s hard to give a single useful definition of “lossy” where there can be such variaety of conversion semantics for conversion between two given types.

Not supporting something like `bignum` is also deliberate: details of those precise semantics might not apply, and general-purpose is hard. A library it free to define its own APIs (including extension traits) for interacting with primitive number types.

3 Likes

That sounds like fallible rounding conversion discussed in Future possibilities.

This is an attempt at describing the existing behavior of `as`. The Nomicon only has this to say: “casting from an f32 to an f64 is perfect and lossless”

That’s what what this RFC proposes. The Alternatives section mentions it.

Sorry, I didn't read that section. But then

In terms of arithmetic, `o` is the only value that `O` can represent such that `o = i + k×2ⁿ` where `k` is an integer and `n` is the number of bits of `O` .

is not well defined.

Edit: I was wrong.

Could you say more about what’s wrong with it?

I see now that fallible float-to-int conversion is listed under future possibilities, and we probably do need an explicit method that matches the behavior of `as`, so I retract my objection. I still feel that fallible float-to-int conversion important enough and essential enough to pursue in tandem with the other methods described here, but it is not a blocker for this RFC.

`approx_to` does indeed sound better than `round_to`.

I feel like that argument is moving the goal posts of what I proposed. What I proposed would be no more generic or general purpose than `TryFrom<usize>` or using any of the `ops` traits both of which are already in the standard library.

I also don't accept the argument of supporting generic numeric code or interoperablity with library numerics being a non-goal when this is not something that is documented and something that was not decided in an open process.

To me this is an argument against including this API in the standard library. General purpose is hard, which is what this API is, even if it's the exact semantics of the `as` operator. So wouldn't it make sense for this API to also belong in `num` and not in std?

I don't see why this would be the special case, yes using the `as` operator is a pain, so is a lot of how the language special cases numerics, and in-completeness of operators having equivalent traits. Why is this one different?

Maybe I missed it, but I think that the conversions from float to int/float towards positive and negative infinity, as well as toward zero are missing. From what I saw, you only propose rounding toward nearest.

So in general this RFC does not immediately propose adding every kind of conversion there is, mostly those that are done with `as` today. And make so that more can be added later.

For those specifically, can they be implemented any more efficiently than chaining with `.ceil()`, `.floor()`, or `.trunc()`? If not, do you still think it’s wort having dedicated APIs?

I did some experimenting in this area and wrote the az crate, which uses traits.

• You can write `i.az::<O>()` or `cast::<I, O>(i)` instead of `i as O`, with overflow panicking with `debug_assertions` or wrapping otherwise.
• There are other traits/casts for checked, saturating, wrapping and overflowing conversions, e.g. `i.checked_as::<O>()`.
• For floating-point to integer conversions I chose truncation by default, but rounding is supported via a `Round` wrapper, so that
``````assert_eq!(0.6f32.az::<i32>(), 0);
assert_eq!(Round(0.6f32).az::<i32>(), 1);
// Ties round to even.
assert_eq!(Round(0.5f32).az::<i32>(), 0);
assert_eq!(Round(1.5f32).az::<i32>(), 2);
``````
• I also wrote conversions into e.g `Wrapping<i32>`, which does wrapping.
• For floating-point to integer conversions I used bitwise manipulation instead of intrinsic conversions as it wasn't clear what the guarantees where; this is probably much slower but every behavior is defined. And I was more interested in the API at this point.
2 Likes

I may miss something, but for me, `.ceil()`, `.floor()` and `.truc()` don't cover the same need.

``````// example with positive numbers
// 10.2 and 10.7 can round to 10 or 11 but not all functions give the same results

assert(approx_to<i32>(10.2_f64) == 10);
assert(approx_toward_infinity<i32>(10.2_f64) == 11);
assert(approx_toward_negative_infinity<i32>(10.2_f64) == 10);
assert(approx_toward_zero<i32>(10.2_f64) == 10);

assert(approx_to<i32>(10.7_f64) == 11); // different rounding
assert(approx_toward_infinity<i32>(10.7_f64) == 11);
assert(approx_toward_negative_infinity<i32>(10.7_f64) == 10);
assert(approx_toward_zero<i32>(10.7_f64) == 10);

// example with negative numbers
// likewise -10.2 and -10.7 can round to -10 or -11 but not all functions give the
// same results (except for the sign) than with positive numbers

assert(approx_to<i32>(-10.2_f64) == -10);
assert(approx_toward_infinity<i32>(-10.2_f64) == -10); // different rounding than with +10.2
assert(approx_toward_negative_infinity<i32>(-10.2_f64) == -11); // different rounding than with +10.2
assert(approx_toward_zero<i32>(-10.2_f64) == -10);

assert(approx_to<i32>(-10.7_f64) == -11);
assert(approx_toward_infinity<i32>(-10.7_f64) == -10);
assert(approx_toward_negative_infinity<i32>(-10.7_f64) == -11);
assert(approx_toward_zero<i32>(-10.7_f64) == -10);
``````

Chaining function may work for `floats` into `integers`, but I don't think it is possible to do the same when converting `f64` into `f32` (or any big floats to a smaller float). Note that I only used integer in the above example, because it's easier to see what the result will be when rounding compared to the same operation with floats.

Also `ceil()` is like `approx_toward_infinity`, but only for positive numbers and it doesn't work for `f64` to `f32`. If you want `approx_toward_positive_infinity` for negative numbers, you need to use `floor()` with the same limitation.

1 Like

One thing that is missing from both the current and proposed APIs is a way to do ergonomic casts that panic on out of bounds / overflow. Yes, I can litter my code with unwrap() calls, but there is a reason that Vec supports indexing rather than requiring get().unwrap() calls everywhere: the common option should ideally have the most concise syntax. This absence is especially annoying for u64 <--> usize conversions since in many cases realistically my code is never going to run on anything other than a 64-bit machine, yet I still have to spend mental cycles thinking about it.

That is not how `.ceil()` , `.floor()` and `.truc()` work, they are exactly what you are asking for. I have seen the names misused in the way you described (old versions of Excel), but most programing language (including Rust) follow their mathematical definition.

There is no equivalent for `f64` to `f32`, but I don't see any use them. If someone can provide a good use then we can consider add them in the future.

2 Likes

Prior art:

That RFC proposed what this RFC proposes, basically, and was rejected because library team wanted to try to do it with a trait first (`TryFrom`).

Differences:

• `cast` was used instead of `to` (`wrapping_to` -> `wrapping_cast`).
• The `Option`-returning operation followed the analogy with arithmetic operations too (`checked_add` -> `checked_cast`, not `try_into`).
• The RFC also introduced a simple `cast` operation that panicked on failure, like simple arithmetic operations (`add` -> `cast`).
• Conversions involving floats weren't considered.

(I still think that RFC did everything right :p)

5 Likes

I would personally really like to see a better casting method for integers. right now you can't to u32->usize or usize->u32 safely. there should be a way to do this conversion with compile time safety (I don't want my code to compile if this can't be done safely).

2 Likes

I suppose would could have modules with names like `at_least_32bit` in `core::arch` and `std::arch` that only exist for corresponding targets. But I think this is unique enough that it deserves to be proposed in its own RFC. So I’m not planning to integrate that idea in this proposal, but feel free to pursue it separately!

In the meantime, consider making your own library (or module) with `cfg` like this:

``````#[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))]
pub fn u32_to_usize(x: u32) -> usize {
x as _
}
``````

With a 16-bit target this function won’t exist, so calling it would cause a compilation error.

2 Likes

Such proposals were deferred before, because Rust is getting portability lints. Once they're implemented, this could be a regular `From` impl.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.