Creating "into operator"

Hi, I really like rust, sometimes writing of something is really long process and one of the idea I had is creating something like "into" operator to make less code (I know rust is safe, but small amount of code can be even safer,...).

for example

instead "my string".into() or String::from(), or f64::from

could be something like this:

  • @"my string"
  • @ <u1 6> 25

... and many more

image

Some opinions:

Rust generally favors explicit over implicit and .into() is already too easy to abuse. I've seen code that over-uses it to the point where it's impossible to figure out wtf is going on without digging deep into the APIs that are being called. I would much prefer people use more specific conversions wherever possible, eg. .to_string() instead of .into(), and for these methods to be added if they don't yet exist. So I really wouldn't like us to make changes which encourage people to (ab)use .into() even more.

Rust has a very high bar for adding new syntax. Using @foo vs foo.into() only saves 6 characters and I can't imagine the devs will consider that enough of an ergonomics improvement to justify adding a new operator.

While smaller code can be safer by being easier to read, more implicit code is often less safe since (a) you're relying on the compiler to correctly infer the intended types and (b) it's harder for the reader to know what the code is doing if the type information is omitted.

If you really want implicit conversions, a common pattern is to have constructor functions that take a generic impl Into<T>. eg.

struct Citizen {
    first_name: String,
    last_name: String,
}

impl Citizen {
    pub fn new(first_name: impl Into<String>, last_name: impl Into<String>) -> Citizen {
        Citizen {
            first_name: first_name.into(),
            last_name: last_name.into(),
        }
    }
}

citizen("John", "Smith")

Though without named function arguments these would (in your case) be easier to accidentally misuse compared to struct literals.

3 Likes

Let's just say it's highly debatable.

What the hell is this even supposed to mean?

The default disposition for any new operators in Rust is NO. Apart from the ? operator, thus far no one has managed to convince otherwise.

1 Like

I guess it is supposed to mean

{ 
    let tmp: u16 = 25.into();
    tmp
}

or if type ascription is allowed everywhere one day:

25.into() : u16

Note that type ascription has been de-RFC'd and most likely won't use this syntax if it is ever stabilized.

1 Like

I still think we should attempt to make expr.into::<T>() work the way people often expect it to.

10 Likes

Agreed 100%. Doing so for Into::into could be hacked by making it a lang item. The problem is generalizing it to be able to provide the generics of the trait when calling a trait method.

I feel like the trait method could simply opt into this behavior. Something like

pub trait Into<T>: Sized {
    fn into< =T >(self) -> T;
}

or some other syntax, specifying “here's another location for explicitly constraining / specifying the type T”.

For most trait methods, the types will be sufficiently constrained by the argument type or existing parameters already, so I don't think it's necessary to come up with any approach that would automatically work with existing trait methods, because most trait methods would never need the syntax in the first place.

Adding such a =T parameter, which would be optional like other type parameters with a default, wouldn't be a breaking change, so any existing trait methods (including Into::into) could add support for this in minor releases.

That could work. My immediate question is what if you did Into::<T>::into::<U>(foo) where T != U? Presumably a hard error for an ambiguous call? Syntax bikeshedding aside.

Whatever solution needs to be reasonably careful, since it's possible to call expr.into::<>() today. Any added generics need to be default inferred.

2 Likes

I’d say, either a conservative error ruling out the call syntactically already, or it would be a type mismatch. If the latter approach is chosen, something like Into::<(Foo, _)>::into::<(_, Bar)>(baz) would probably also be expected to work and unify accordingly; same for redundantly specifying the type twice. Redundantly specifying types that are already inferrable is common in Rust anyways, e.g. for type signature on variables in many cases. Incorrectly specifying the type of a variable leads in a type mismatch error.


One somewhat comparable thing is how enum variants can get their arguments in two possible places. They seem to be using a syntactical criterion.

let x: Option<()> = Option::None;
let x: Option<()> = Option::None::<>;
let x: Option<()> = Option::<>::None;
let x: Option<()> = Option::<>::None::<>;
let x: Option<()> = Option::<()>::None;
let x: Option<()> = Option::<()>::None::<>;
let x: Option<()> = Option::None::<()>;
let x: Option<()> = Option::<>::None::<()>; // fails
let x: Option<()> = Option::<()>::None::<()>; // fails

Rust Playground

Edit: Ah, didn't notice rustfmt “fixing” my test cases with empty <>s. Fixed now.

1 Like

I just had written a lengthy response that was somewhat nonsensical, since I forgot that functions do not support defaulted arguments in the first place :sweat_smile:

I mean, perhaps it was not completely nonsensical, since defaulted parameters are a reasonable future language addition, and we wouldn’t want to introduce any unnecessary extra complications for such a feature. The basic idea is that if the trait parameters become implicitly added defaulted type parameters to trait methods, then adding normal defaulted parameters (if we get such a feature) would be a breaking change, since it mixes up the order. Well… or, if the order is different, then adding defaulted parameters to the trait would be breaking. Problematic either way.


Indeed this feature would add a first case where a function would have default arguments. There are no semantic questions as to how to handle those in this case though. Usually, the problem is that it’s unclear for functions when the defaults should actually be considered. In this case, the default is to leave the parameter “inferred”. (It is always unified with the trait’s parameter that it’s set to be equal to anyways.)

Makes me wonder: Are there existing proposals for adding something like

fn foo<S, T, U = _>() {}

i.e. type parameters whose default value is “leave this inferred”? Same argument as above, there would be no hard semantic questions to answer for such a feature. It would seem useful to me for making API such as Iterator::map nicer to call, since you wouldn’t need to specify the closure type anymore: .map::<Ty, _>(|x| …) (helps inferring the return type of the closure as Ty) would become somewhat neater, writable as .map::<Ty>(|x| …).

It would also serve as a way of converting impl Trait parameters into named parameters without breakage.

Apologies if this was already proposed, but would it make sense instead to have something like this?

trait IntoExt {
    fn to<Other>(self) -> Other where Self: Into<Other>;
}

impl<T> IntoExt for T {
    fn to<Other>(self) -> Other where Self: Into<Other> {
        Into::into(self)
    }
}

Generic bounds stay the same (you still require From/Into) but you'd be able to write

let n = 3u8;
let m = n.to::<u32>();

The trait would not be imported by default, but in a new epoch it could be added to the prelude.

Unfortunately the function cannot be called IntoExt::into because it conflicts with the existing Into::into

3 Likes

I actually published an implementation of such a trait some time ago as a crate.

IIRC I've also seen other instances of similar traits in some utilities crates.

Fascinating stuff… looking back at my own crate, I’m noticing:

I use a signature of the form

fn into_<T>(self) -> T
where
    T: TypeIsEqual<To = T_>
{ … }

where T: TypeIsEqual<To = T_> is a library-implemented approximation to something like a T == T_ bound.

So now I’m realizing that something like

trait Trait<T> {
    fn foo<S, =T>();
}

would be equivalent to

trait Trait<T> {
    fn foo<S, T1 = _>()
    where
        T1 == T;
}

with proper == bounds, so that such a =T feature becomes redundant if we get == in where clauses as well as = _ defaults.

Also, for methods with previously no type arguments, the = _ is kind-of redundant, since existing calls like

will not break, because ::<> is kind-of ignored by the compiler anyways.


I must say though that

trait Trait<T> {
    fn foo<S, T1 = _>()
    where
        T1 == T;
}

is IMO slightly more confusing to look at, as it involves a fully-fledged extra type argument and a constraint, whereas the interpretation of what this means is mostly for syntactical reasons/convenience; so even with this insight, =T could be a reasonable feature even if it’s just syntactic sugar.

1 Like