"into" function parameters

i very often find myself doing the following for ergonomics:

fn foo(a: impl Into<A>, b: impl Into<B>) {
  let a: A = a.into();
  let b: B = b.into();
  // ...
}

since rust doesn't allow for function overloading (which i think is a great decision), we have to reach for traits such as Into, TryInto, AsRef and etc when we want to allow our function to take more than one type, it's a clever pattern, but it can become boilerplaity and annoying to write.

I wish we had syntactical sugar for this, something like:

fn foo(a: into A, b: into B) {
  // ...
}

the above code would be equivalent to the previous foo fn. into would be a new keyword that could be applied to fn parameters, it would accept any type that implemts Into<T>, and would perform the conversion implicitly

I've used this pattern myself! I have some functions that are meaningful to optimize by monomorphizing them with a constant input (I tested), but can't be inlined, so I gave them exactly such a generic parameter, so you can pass a ZST that represents a constant value.

Unfortunately, although the boilerplate is annoying, I don't think it's worth adding a language feature for it. This is a relatively rare pattern with only a small amount of boilerplate, and such a feature would add to the complexity of the language (creating more work for both Rust learners and procedural macro writers).

It would be possible to implement this as a procedural macro, though. If you have a project where you use this a lot, and are willing to dig into the complexity of writing procedural macros, you could make a macro that lets you do this:

#[auto_into]
fn foo(a: into A, b: into B) {
  // ...
}

or perhaps this, so that you don't have to complicate the parsing:

#[auto_into]
fn foo(a: impl Into<A>, b: impl Into<B>) {
  // the macro just automatically inserts the lines:
  // let a: A = a.into();
  // let b: B = b.into();
  // ...
}
5 Likes

When I first read this, I thought you were referring to the need to possibly even define your own traits in cases where standard library ones don’t suffice, and/or at least the need to write trait impls for relevant types.


Just for writing the function signature, the sugar impl Into<A> is already sufficient, and also way more general than something like “into A” involving a new keyword and working only with one single trait (Into).

Admitted… the implicit conversion part does remove some relevant boilerplate, admitted; but the disadvantage is that you don’t control the .into calls so well anymore… the order would be chosen by the compiler, and the calls would happen unconditionally at the beginning at the function. Maybe still fine for Into, but you also mention TryInto which you would need to do error handling for (or keep a Result in the variable?)… and as indicated above, if the proposal is to special-handle only Into, then I’d say, Into is not really special enough, and also – in my opinion – possibly intended more for manual calls than for function signatures anyways, one reason being that the call can be a bit costly (e.g. involving allocations and sometimes copying data). (E.g. there’s no impl Into function arguments in standard library functions as far as I’m aware, whereas AsRef ones do exist, particularly around file path APIs.)

To counter the point of usefulness of removing

  let a: A = a.into();
  let b: B = b.into();

boilerplate: quite often you don’t even need it; particularly in case the resulting value has only a single use, it can be inlined there and wouldn’t need the new variable a.

Also, Rust’s macro system is quite powerful; I assume it’s quite straightforward (and perhaps a fun exercise if you do find a lot of use-cases) to write a proc-macro using the syn crate that can transform something similar-looking, e.g. something as follows

#[into_sugar]
fn foo(a: #[into] A, b: #[into] B) {
  // ...
}

into the desugared code you want

fn foo(a: impl Into<A>, b: impl Into<B>) {
  let a: A = a.into();
  let b: B = b.into();
  // ...
}
4 Likes

As a (way more) general approach, I believe there’s value in some “view-pattern” (stealing Haskell terminology) type feature of applying functions (and possibly pattern matching the result) in patterns. And also a pattern: Type syntactic element could be made to support being generated by a single macro call.

If this became a thing, then some syntax… let’s call it “Into(x)” as a stand-in (or maybe even an actual user-defined “pattern alias”) could be a pattern that applies into(); i.e. let Into(x) = foo(); is like let x = Into::into(foo());, and – if necessary – a trivial macro_rules macro could translate something like foo(into!(a: A)) into foo(Into(a): impl Into<A>).

The downside to view patterns (and maybe a reason we don’t have them? I would need to look for relevant discussions…) include the abovementioned aspects of giving up control and not allowing error handling.[1]


  1. In comparison, Haskell (a purely functional language) doesn’t have side-effects, and has lazy evaluation, so giving up explicit control about when which things are executed is kind of universally the case anyways, and there aren’t relevant side-effects to worry about either, at least making the order of execution fairly irrelevant. ↩︎

1 Like

Writing the signature as fn foo(a: impl Into<A>, b: impl Into<B>) is an antipattern. You turn a perfectly fine function with concrete type signature into a generic function, which can no longer do type inference (because it isn't known what should be the type which impls Into), causes binary bloat and compile time bloat, and can't be used in many contexts, such as methods on trait objects, extern functions or function pointers.

Very rarely it is indeed the best solution, but if you're writing it often enough to want some minor syntax sugar (4 characters instead of 10), then you're designing you API wrong. It's not an issue to call into() conversion at use site, it gives more flexibility to the consumer, it doesn't have any syntactic, runtime or compile-time overhead if your already have a variable of correct type, and shouldn't need to happens that often in the first place, if you choose the argument types properly.

If you really often need to use such generic arguments, then there is something big going on, which likely demands a proper solution: enums for option enumeration, or special-case traits which convey the semantics of your operations and are much more controllable than Into.

3 Likes

BTW, you don't need the type hint in let, because generics eliminate all other possibilities, so there's only one unambiguous into() that you can call on the generic type.

1 Like

Remember that you're paying a potentially-not-insubstantial compile-time and monomorphization-bloat cost to save a .into() in the caller when doing this. Personally I think it's often an anti-pattern.

If you choose to use a macro for this, be sure it desugars not into

fn foo(a: impl Into<A>, b: impl Into<B>) {
  let a: A = a.into();
  let b: B = b.into();
  … body …
}

but into

fn foo(a: impl Into<A>, b: impl Into<B>) {
  return inner(a.into(), b.into());
  fn inner(a: A, b: B) {
    … body …
  }
}
11 Likes

I don't know what category this falls into, but this would be a fantastic code size optimization(?) pass.

1 Like

CString in std::ffi - Rust is one that I am aware of.

1 Like

The general pattern is polymorphization (A-polymorphization, wg-polymorphization), and rustc does have the experimental capability to do some MIR polymorphization behind the -Zpolymorphize flag.

2 Likes

Thanks, TIL.

Relatedly, there's a macro crate that converts the first form into the second form (roughly speaking):

https://llogiq.github.io/2019/05/18/momo.html

7 Likes

you know what, fair enough. guess there was a lot i didn't know about and i see now why this wouldn't be a desirable feature.

however, i did write that macro: auto_into

3 Likes

wait, what's the diference between the 2 code examples here?

In the first one, foo is a gigantic function and is copied for each distinct input types given. The second is a single inner function for A and B specifically with some tiny glue code that is copied for each distinct input types given.

9 Likes

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