Move the generic type from the trait Into signature to the into method

Trait Into is not convenient.

Introduction

One of the most frequently used scenarios with trait Into is converting one type into another.

// point is of type Point
let vector: Vector = point.into();

This code works thanks to the automatic type determination mechanism. In this case, the Vector type is explicitly annotated, and the Into trait uses the corresponding implementation of Into for Point.

Problem

However, there are situations when automatic type determination does not work.

For example: SomeOfVector::from(point.into())

In such cases, it is necessary to explicitly specify the particular implementation of Into for Point:

SomeOfVector::from(<Point as Into<Vector>>::into(point))

This expression turns out to be redundant and difficult to read.

Solution

To simplify usage, one can shift the responsibility for specifying the particular implementation of Into from the trait signature to the method into signature. This is also semantically more correct. Instead of:

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

One should use:

pub trait Into {
   fn into<I: From<Self>>(self) -> I
      where
        Self: Sized;
}

Just like in the standard implementation, it is necessary to implement Into for each type that has a From implementation:

impl<F> Into for F {
   fn into<I: From<F>>(self) -> I {
      I::from(self)
   }
}

Now type conversion becomes simpler and cleaner for any usage scenarios:

let vector: Vector = point.into();
let vector = point.into::<Vector>();
SomeOfVector::from(point.into::<Vector>())
3 Likes

That's a massively breaking change.

There could be a standalone, turbofishable generic function in std, though.

4 Likes

Is there a reason you can’t just use From instead when you need to annotate the target type?

SomeOfVector::from(Vector::from(point))
9 Likes

The reason is solely convenience. Your question seems to rather challenge the existence of the Into trait itself; I am merely suggesting its convenient implementation. Instances of using Into instead of From are purely idiomatic, and I would not like to forsake them due to an inconvenient implementation.

Here are examples where it can be convenient:

.map(|rect| rect.into::<[Rc<Triangle>; 2]>())

(Line::from([a, b]) - vector).into::<[Rc<Vector>; 2]>()
let [a, b, c] = [
   self.cab.ba.a.into::<i32>(),
   self.abc.ba.a.into::<i32>(),
   self.bca.ba.a.into::<i32>(),
];

The example I provided (SomeOfVector::from(point.into::<Vector>())) is also semantically convenient when we say that we are transforming a point into something that SomeOfVector consumes.

2 Likes

No, .into() is useful in places where inference can handle it. There's a similar relationship between .collect() and Vec::from_iter, if inference works .collect() is simpler and shorter, if it doesn't then Vec::from_iter is normally better. collect does agree with your premise that having the generic on the method is more useful, as there are a few rare situations where it can be preferable, but how rarely that happens IMO means trying to do anything in the standard library to change this is not worth it (even if some non-breaking change is found).

I would personally not use turbofished-into for any of the situations you highlight

// these first two would likely have shorter type aliases
// avoiding the outer `<>` if they are common types

.map(<[Rc::<Triangle>; 2]>::from)

<[Rc<Vector>; 2]>::from(Line::from([a, b]) - vector)

let [a, b, c]: [i32; 3] = [
   self.cab.ba.a.into(),
   self.abc.ba.a.into(),
   self.bca.ba.a.into(),
];

(There was a reason for the separately implementable trait in the past, which made a generic method impossible, but afaik since the orphan rule changes there's no situation where you should be implementing Into directly).

5 Likes

A possibility would be to modify the language so that trait generic arguments can be specified on method calls, as long as the method has no generic parameters itself. This would allow .into::<T>() while remaining compatible; it would also work for other sometimes-ambiguous methods like AsRef::as_ref() and Borrow::borrow().

There is even a sort of precedent for this syntactic relationship: generic arguments to an enum can be specified when naming a variant of the enum (e.g. None::<usize>), as I was just reminded by @steffahn's URLO post today.

However, this would be ambiguous if we wanted to also add

  • type parameter defaults for functions, and
  • official sealed traits,

because then it would be considered backwards-compatible to introduce a defaulted type parameter to a sealed trait's method, changing the meaning of the generic arguments on the call.

9 Likes

It's possible to just identify the trait without naming the Self type:

Into::<Vector>::into(point)

A small improvement, but still more verbose than a turbofish on the function call.

4 Likes

I think opt-in is optimal to avoid any breakage. E.g. if we had a feature of declaring a type parameter “=Other” that just unifies with another existing type parameter, we could changer Into to be defined as

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

(I don't really care about the precise syntax, and a MVP might as well just cover the case of supporting nothing but a trait’s parameter being repeated on a trait item.)

The functionality of this would be that into gains an optional type parameter and if this parameter is explicitly given, the given type is simply unified with the parameter in question. If nothing is explicitly given, then it’s the same as if _ was given, which contributes nothing to restricting or disambiguating the parameter in question.

Edit: Also, it would need to be allowed to not repeat the declaration of such a =T parameter in a trait implementation for Into.

E.g. the most complex case of unification would also support fun cases like

let x: (bool, _, _) = Into::<(_, i32, _)>::into::<(_, _, f64)>(foo);

and the compiler would combine all the type information to deduce that the target type is (bool, i32, f64); though the more common use-case of course would be of the form

let x = foo.into::<(bool, i32, f64)>();
7 Likes

I have mixed feelings about this because it adds even more syntax variations to generics, which already are a complex feature (deciding between args on the type, or associated types, or args on methods), and even methods alone already have syntax with multiple confusingly similar ways of doing the same thing (e.g. generic args before function arguments vs impl Trait in args vs where clauses). So yet another variant of turbofish and yet another combination of arguments on type vs method make already messy situation even messier.

I’d prefer some different approach. Maybe add operators for into and try_into that could include type that looks more like a type cast. Maybe revisit type ascription with a new syntax.

4 Likes

I've thought about this too when I observed that we can further restrict generics in trait methods using where.

trait Trait<T> {
    fn method<F>() -> F
        where T: AnyTrait;
}

Then why couldn't we also specify the impl more concretely when calling. For example, it could be possible to reassign generics:

trait Trait<T> {
    fn method<T>() -> T;
}

But this would not be very explicit.

Then it could be possible to limit generics by other generics:

trait Trait<T> {
    fn method<F: T>() -> F
}

saying that F is the same as T, if F is specified when calling the method, it shadows T.

1 Like

You can maybe do this:

pub trait Id {
    type This;
}

impl<T> Id for T {
    type This = Self;
}

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

    fn into_generic<R: Id<This = T>>(self) -> T {
        self.my_into()
    }
}

Playground

FWIW, I do feel like this is a problem worth solving. It's a bummer that when type inference fails, you're also deprived of the ability to use method chaining.

I don't know that any of the solutions proposed so far are worth the cost, though… I feel like there should be a more "thorough" approach to allowing things in postfix notation, but I'm not sure what it would be

6 Likes

A postfix type ascription syntax would solve this too: x.into().as::<i64>()

1 Like

In Weird syntax idea(s) for UMCS , there was a proposal for syntax that would let you call any function as if it was a method of its first parameter. In summary, x.(foo)(y) would be like foo(x, y), except with the usual auto-(de)refs applied to x.

Something like that would be primarily useful for functions that aren't methods, but should also allow using the fully qualified syntax within method chains:

42
.(<i8 as Into<i16>>::into)()
.(Into::<i32>::into)()
8 Likes

Just goes to show how good was this idea, did anyone write an RFC about it?

I believe not. I'm a bit burnt out on putting forward actual proposal RFCs without preexisting team buyin, as that's mostly just a good way to put in a lot of effort for no result except a PR sitting open for years.

That said, I do think it's still a decent idea and would help write up the full RFC given someone on T-lang offering to shepherd it to get discussed and/or a compiler contributor offering to implement it experimentally.

into/from was actually on the mind when I wrote the idea out, although I'd expect num.(i32::from)() more than num.(Into::<i32>::into)(). These should access the same set of possible impls, would were Into have the generic on the method, and ? using From encodes the expectation into std that From is the trait to use, Into the (usually) convenient postfix method syntax sugar.

Aside: there's some momentum behind wanting to be able to do use Default::default. While Into::into::<T> is unlikely to just be possible, use Trait::method would synthesize a free function by prepending any trait generics to the method's generic list. Option::<>::None is a non-generic unit variant. Option::* is special and different from Option::<>::*, but Trait::* is always Trait::<>::*. (Bonus fun: ::<> isn't "zero generics," it's "infer any/all generics," i.e. identical to no turbofish for any item other than a generic enum.)

One of the interesting interactions to work out there would be with designs like Arc::into_inner which is deliberately not a method to avoid any ambiguity with the inner item. The goal is to force you to write Arc::into_inner(arced) if you really want to use that functionality, and not arced.into_inner().

I'm confident that it's a fixable interaction, but it would need some thinking about - not least asking the question whether this is simply an acceptable loss of expressiveness to enable more expressive code elsewhere?

It seems to me this proposal would allow arced.(Arc::into_inner) and that's perfectly okay because it's unambiguous.

4 Likes

Instead of modifying the Into trait, which would be breaking; or coming up with additional syntax, which would take a lot of time to design properly; we can also just add a new trait.

An example of this is in the tap crate. Which offers the Conv trait.

trait Conv {
    fn conv<T>(self) -> T
    where
        Self: Into<T>,
        T: Sized,
    {
        self.into()
    }
}

impl<T> Conv for T {}
3 Likes

What about allowing

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

With the semantics that the method has a type parameter T2 that would default to T.