Closure as argument. Parameterization with `impl Trait`

Yes. Incindentally, this is why I think impl Trait in argument position was a mistake. It is completely different from impl Trait in return position, and that in itself is confusing. I usually recommend avoiding it altogether, and only using this syntax in return position.

5 Likes

Is that different in argument position? In the case of argument position caller neither can't define the type of such argument. Or can? I am still confused.

The caller can indeed choose it when its an argument:

use std::fmt::Debug;

fn print_me(value: impl Debug) {
    println!("{:?}", value);
}

fn main() {
    print_me("foo_bar"); // here it's a &str
    print_me(2384); // here it's an i32
}
1 Like

fn f(_: impl Trait) {} is similar to fn f<T: Trait>(_: T) {}, where it is generic and monomorphizes, in contrast with being concrete and inferred. However,

  • It's opaque (no name), even to the function writer (e.g. they cannot call T::trait_method())
  • Callers can't use turbofish notation: f::<SomeT>()

The latter makes it a breaking change to go from <> to argument position impl Trait.

1 Like

In argument position impl Trait, the caller determines the concrete type. (If neither the caller nor the callee could, then who can?)

1 Like

Related question to clarify the space of possibilities.

Another way of seeing these things, which is mentioned in the impl Trait RFC itself, and which I've found of way better clarity, is to consider any MyTrait and some MyTrait as the syntax used for impl MyTrait, which makes it explicit whether it is universal / generic / caller-chosen or of it is existential / callee-chosen:

fn f<T : Display>(arg: T)

is —modulo turbofish—, equivalent to:

fn f(arg: impl Display)

which, with the syntax I've mentioned, would be written as:

//! Pseudo-code!
fn f(arg: any Display)
  • In all these cases, the caller picks the specific Displayable type, since the function can handle anyone of them:

    f(42_i32); // OK, `i32 : Display`
    f(true); // OK, `bool : Display`
    

Then, similarly, but in return position this time, consider:

fn g<T : Default>() -> T {
    <_>::default()
}
  • With it, the caller picks the return type:

    let default_i32: i32 = g();
    let default_bool: bool = g();
    
    enum NotDefault { A, B }
    let error: NotDefault = g(); // Error, caller can only pick a type that implements `Default`
    

would be:

//! Pseudo-code!
fn g() -> any Default {
    <_>::default()
}

Which is different than:

fn g() -> some Default {
    42_i32 // the type `i32` is indeed `Default`
}

or

fn g() -> some Display {
    42_i32 // ditto
}

both of which are, in practice / actual Rust, written as -> impl …:

fn g() -> impl Display {
    42_i32
}

and which feature the semantics of some single type —about which we only know that it is Displayable—, which is chosen by the callee: g() always returns the same type, no matter how the caller calls it.

With an extra nuance: for the sake of future-proofing the APIs and avoid breakage, when a callee defines a function as returning some Display, they never promise / tell which one it is concretely.

So, in the example:

// fn g() -> some Display {
   fn g() -> impl Display {
       42_i32
   }

a caller cannot rely on the "some Display type" being i32, even if it technically is that type. That is, Rust will hide / encapsulate the return type in an opaque layer that only let's the Display aspect of the concrete return type slip through:

let n: i32 = g(); // Error, expected ("unknown" caller-chosen) `some Display`, got `i32`.
4 Likes

Amazing! Thank you :smiley:

//! Pseudo-code!
fn g() -> any Default {
    <_>::default()

Type depending on value? Kind of second-order typed lambda calculus?

That particular function is nothing new as far as I can tell – it would be equivalent with fn g<T: Default>() -> T.

2 Likes

@Yandros @H2CO3 @quinedot @alice I am writing an article based on the discussion. Is anyone interested giving me feedback on that? Please :slight_smile:

Sure!

@Yandros @H2CO3 @quinedot @alice Any feedback, please.

1 Like

You should give your image a background instead of transparency (or otherwise make it easier to read on both light and dark modes).


You say

Parametrization of the result type with impl Trait solves the problem.
[...]
Is not impl Trait a form of parametrizing a function equivalent? It is, but not identical.

referring to

pub fn problem() -> VectorFormer< impl Fn() > { ... }

and while the return type (VectorFormer<T>) is being parameterized in this case, this still made me pause a bit, as I wouldn't call return-position impl Trait a parameterization of the function in the general case.

// There is only one `f`
fn f() -> impl Display { ... }

I don't know that I would call your "Corner case" section a corner-case. It's more a fundamental property of return position impl Trait -- it's a concrete type, not a dynamic type or generic type [constructor].


Another possible follow-up would be to compare and contrast dyn Trait.


return-position impl Trait (RPIT) is part of a much larger surface area in Rust: type alias impl Trait (TAIT) and generic associated types (GATs). However, those others are not yet stable. But we're getting relatively close.

RPIT in generic context

You can return an impl Trait in a function that has input type parameters:

fn f<T: Debug>(t: T) -> impl Debug {
    vec![t]
}

This means that the opaque, concrete output type can depend on the input type to the function. Basically, the output type is parameterized by the same inputs as the function. Given concrete values for all the inputs, the output type is also concrete.

Introduction to type aliases

In case you didn't know, you can write

type MyInt = i32;

and MyInt will be an alias to i32.

These aliases can be generic too.

type Container<T> = Vec<T>;

Connection of RPIT to TAIT

The RFC mostly uses a different syntax, but basically there will be a way to declare something like

type MyDisplay = impl Display;

And then have a defining use somewhere, but this time it's reusable:

fn f() -> MyDisplay { 0i64 }
// Still a concrete, albeit opaque, type.  They must match.
fn g() -> MyDisplay { 42i64 }

These aliases can be generic, too:

type MyDebug<T: Debug> = impl Debug;
fn h<T: Debug>(t: T) -> MyDebug<T> { vec![t] }

And in fact, the same machinery drives return-position impl Trait today. Those are just generated by the compiler and don't have a name (alias), and are thus not "reusable".

Connection between (non-generic) type aliases and (non-generic) associated types

Associated types of traits are basically an implementation-specific type alias (that can also be bounded on).

trait Foo<T> { type Bar; }
impl Foo<i32> for str { type Bar = f32; }
impl Foo<u32> for str { type Bar = String; }
impl Foo<u32> for String { type Bar = (); }

And you can use this to have a method that has a concrete type per implementation, but may differ across implementations. The type can also be bound.

trait MakeDisplay {
    type Output: Display;
    fn make_it() -> Self::Output;
}

Note that the type must be name-able (so you can write type Output = Name;, and is not opaque to users (who can refer to <Implementer as MakeDisplay>::Output).

Also note that today, unlike type aliases, associated types cannot be generic.

Generic Associated Types

GATs are the natural extension of associated types to be generic, as type aliases are.

I don't know of any specific acronym for it, but another natural extension will be to support full TAITs on GATs. GATAIT?

RPIT in traits (RPITIT?)

Once you have GATs with TAIT, you can support return-position impl Trait within a trait itself. The compiler generates an anonymous GAT defined as an impl Trait within the trait implementation, similar to how RPIT today generates an anonymous TAIT.

Summary

Thing Example Notes Stable?
type alias type X = Y :ballot_box_with_check:
generic type alias type T<X> = Y<X> :ballot_box_with_check:
TAIT type T<X> = impl Tr<X> :x:
RPIT fn f() -> impl Tr anonymous TAIT :ballot_box_with_check:
associated type impl trait Tr for U { type T = Y } :ballot_box_with_check:
GAT impl ... { type T<X> = Y<X> } :x:
GATAIT impl ... { type T<X> = impl Tra<X> } :x:
RPITIT impl ... { fn f(&self) -> impl Tra } anonymous GATAIT :x:
5 Likes

To illustrate:

Very nice post & summary :ok_hand:. This type of "increasing complexity" is begging for a meme:

3 Likes

Thank you :blush: that's so valuable for me that you guys having such tasks pressure found time to review my article.

How would you call it?

how would you call that?

They are both outcomes of RPIT being a

  • type alias
  • of an output type, or of part of an output type
  • in a statically typed language

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.