So, I made some experiment to revive this discussion.
My goal was to replace one of the most popular numeric conversions in the Rust codebase -
as usize
- with three semantically different actions: widening (lossless conversion), truncation (potentially lossy conversion) and sign conversion (potentially lossy) and to analyse the result.
This was done with three simple traits - Widen
, Truncate
and ConvertSign
:
trait Widen<Target>: Sized {
// Lossless numeric conversion (equivalent to operator `as`)
fn widen(self) -> Target;
}
trait Truncate<Target>: Sized {
// Numeric truncation (equivalent to operator `as`)
fn truncate(self) -> Target;
}
trait ConvertSign: Sized {
type TargetSigned;
type TargetUnsigned;
// 2's complement sign conversion or no-op for signed numbers (equivalent to operator `as`)
fn as_signed(self) -> Self::TargetSigned;
// 2's complement sign conversion or no-op for unsigned numbers (equivalent to operator `as`)
fn as_unsigned(self) -> Self::TargetUnsigned;
}
My branch with results can be found here:
and the diff between the branch and the upstream is
Some notes:
-
Widen
andTruncate
really have to represent "weak" widening and truncation an not "strict", i.e. bothWiden<T>
andTruncate<T>
are implemented for the typeT
itself. This is for portability reasons, both between 64-bit and 32-bit machines and different operating systems. (The first couple of commits in my branch show how it looks whenWidenStrict
andWidenWeak
are separate traits.)
For the same reasonsConvertSign
is a single trait and not two -AsSigned
andAsUnsigned
-as_unsigned()
(as_signed()
) should be supported for unsigned(signed) types too (but be a no-op, obviously). -
Widen
andTruncate
are identical tostd::convert::Into
in their form (see below).
Conclusions:
- In general I like the result - "harmless" conversions like widening, that you can pretty much throw anywhere without much thinking (but I'm still against making them implicit), are clearly separated from "suspicious" conversions, that require some thinking and analysis.
Lossless conversions are much more common than lossy - the statistics in my experiment is definitely skewed here, becauseusize
is a relatively wide type, but remember thatas usize/isize/u64/i64
are the most popular conversions and they tend to be widening, and conversions to narrower types are much rarer.
It means that programmer's attention can be re-targeted from conversions with operatoras
in general to only their small but potentially more problematic portion.
Moreover, with dedicated semantically loaded methods for certain conversions the all-purpose operatoras
itself would be used rarer and could be considered "raw and low level" and requiring more attention.
However, there are some (solvable) problems, diminishing the usefulness of the traits today:
-
Default type parameters don't drive type inference. It means that you can't do the next thing:
let a: u16 = 10; let b = c[a.widen()]; and you have to give a type hint to
widen()
somehow.
(In my branch the type hint is given with an additional trait methodwiden_
which is clearly a hack and shouldn't be there in the final design).
With improved type inference based on default type parameters type hints forwiden()
would almost never be needed. -
Type ascription is not implemented. Even without improved type inference you could supply a target type with type ascription, but there's no easy and short way to do it without it. (
Into
has the same problem currently.)let a: u16 = 10; let b = c[a.widen(): usize];
-
widen()
is better and safer way to perform conversions, but to compete with core language facilities likeas
it should be really convenient to use.
As a minimum theWiden
trait should live in the prelude, besides that the method call.widen()
is quite long to type (although not as long asas usize
) and may be shortened somehow. -
While
widen
is almost unquestionable, the other methods can raise some questions - for example, how lossy conversions should be treated, with silent truncation (like operatoras
), with panic (like arithmetic operations) or the methods should returnOption
.
All these notes, conclusions and problems led me to one alternative: don't use a separate trait Widen
for lossless numeric conversions, but use Into
instead.
Pros:
-
Into
is already in the prelude and.into()
is shorter than.widen()
-
Into
will probably be implemented for integer conversions anyway, because they are perfectly valid safe conversions, andInto
is idiomatic for such conversions, and they can possibly benefit from generics usingInto
. - There's a non-zero chance, that
Into
and its friends fromstd::convert
being such basic traits will get some short and convenient language sugar some day in the future.
Cons:
-
.into()
is not as semantically clear as.widen()
, but it may be treated just as "safe and lossless type adjustment" without the widening aspect
So, here's some practical actions that I propose:
- Implement
Into
/From
for lossless numeric conversions - Postpone implementing the other mentioned traits (
Truncate
,ConvertSign
) for some time, the "raw and low level" operatoras
still can be used for them.