New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: impl trait expressions #2604

Open
wants to merge 3 commits into
base: master
from

Conversation

Projects
None yet
@canndrew
Contributor

canndrew commented Dec 3, 2018

Rendered view

This is an idea I've seen floating around for a while. I like it, so I decided to give it a proper RFC.

Summary: Add impl Trait { ... } expressions as a kind-of generalization of closure syntax.

Show resolved Hide resolved text/0000-impl-trait-expressions.md Outdated
Show resolved Hide resolved text/0000-impl-trait-expressions.md Outdated
though having to explicitly declare a type in these situations can be
unnecessarily painful and noisy. Closures are a good example of how
this problem can be ameliorated by adding the ability to declare once-off
values of anonymous types.

This comment has been minimized.

@Centril

Centril Dec 3, 2018

Contributor

The motivation feels a bit thin; it would be good to work in some real world use cases of where it would be beneficial; I'm sure you won't have any problems doing that.

let foo = move || println!("{}", y);
```
With this RFC, the above code becomes syntax sugar for:

This comment has been minimized.

@Centril

Centril Dec 3, 2018

Contributor

I don't think calling it sugar does it justice; in particular, if you add type parameters to a closure, there's type inference going on to infer what the type of Self::Output is as well as what Args is.

};
```
Which, in turn, is syntax sugar for:

This comment has been minimized.

@Centril

Centril Dec 3, 2018

Contributor
Suggested change Beta
Which, in turn, is syntax sugar for:
Which, in turn, is syntactic sugar for:
let foo = move || println!("{}", y);
```
With this RFC, the above code becomes syntax sugar for:

This comment has been minimized.

@Centril

Centril Dec 3, 2018

Contributor
Suggested change Beta
With this RFC, the above code becomes syntax sugar for:
With this RFC, the above code becomes syntactic sugar for:
This feature is fully described in the guide-level explanation. As this is a
generalisation of the existing closure syntax I suspect that the implementation
would be fairly straight-forward.

This comment has been minimized.

@Centril

Centril Dec 3, 2018

Contributor

I'd like at least the following to be discussed:

  1. What is the interaction with #2229?

  2. What changes are there to the grammar? and are there any ambiguities (there are, see below)

  3. What happens when I implement the Fn trait and it is a subtrait once removed of FnOnce? Do I also get the supertrait impls?

  4. How do I implement several traits at once? Can I do that? This ties into 3.

  5. What is the type of an anonymous impl? A voldemort type?

  6. Will Copy and Clone be implemented for the impl trait expression if possible like for closures?

  7. What happens if I write let obj = impl Foo<'_> { ... };? Normally this would quantify a lifetime 'a so we'd have impl<'a> Foo<'a> for X { ... }...

  8. What rules if any apply for turning let x = impl Foo { ... } into a trait object?

This feature is fully described in the guide-level explanation. As this is a
generalisation of the existing closure syntax I suspect that the implementation
would be fairly straight-forward.

This comment has been minimized.

@Centril

Centril Dec 3, 2018

Contributor

As for the ambiguity aforementioned, consider:

fn foo() {
    struct X;
    
    impl X {
        fn foo() {}
    };
}

This compiles today and impl X { ... } is an item followed by the empty statement ;.

According to your RFC however impl X { ... } is an expression which is made into a statement by the following ;.

This is not an insurmountable challenge but you'll need to think about how to deal with it.

This comment has been minimized.

@Centril
# Prior art
[prior-art]: #prior-art
Other than closures I'm not aware of any prior art.

This comment has been minimized.

@Centril

Centril Dec 3, 2018

Contributor

Some prior art to consider:

  • Java's anonymous classes

This comment has been minimized.

@clarcharr

clarcharr Dec 3, 2018

Contributor

Kotlin and C++ also have a similar feature to Java's.

@burdges

This comment has been minimized.

burdges commented Dec 4, 2018

It's too bad this never came up before -> impl Trait because this would've provided an excellent reason for the -> some Trait syntax or whatever.

@eddyb

This comment has been minimized.

Member

eddyb commented Dec 4, 2018

My version of this general idea (predating -> impl Trait IIRC):

fn main() {
    let world = "world";
    let says_hello_world = struct {
        world,
    } impl fmt::Display {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "hello {}", self.world)
        }
    };

    println!("{}", says_hello_world);
}

The struct {...} would be an "anonymous" struct (not a structural record, more like a closure, with its own identity, different from all other struct {...} expressions).
The advantage, IMO, is that passing the "captures" around is explicit and works for more situations (whereas closures are more limited):

struct {
    x,
    y,
} impl Default {
    fn default() -> Self {
        Self { x: 1, y: -1 }
    }
} impl Clone { // imagine `#[derive(Clone)]` generating this:
    fn clone(&self) -> Self {
        Self {
            x: self.x.clone(),
            y: self.y.clone(),
        }
    }
}
@mark-i-m

This comment has been minimized.

Contributor

mark-i-m commented Dec 4, 2018

I could be persuaded to be in favor of this, but my inclination is against it just because of Java. Anonymous classes in Java are huge source of noise and hard-to-understand code IMHO.

In addition, I would like to see the following addressed in the RFC:

  • Documentation: do anonymous structs show up in rustdocs? If so, what do they look like?
  • Debugging and name mangling: debugging and printing error messages with unnameable types is a pain. What does that look like in this case? Additionally, what needs to be added to a name-mangling scheme like #2603 for this to work?
@sfackler

This comment has been minimized.

Member

sfackler commented Dec 4, 2018

@mark-i-m I'd expect the answers to both of those questions to be identical to the answers for closures.

```rust
let y = String::from("hello");
let foo = move impl FnOnce<()> {

This comment has been minimized.

@ExpHP

ExpHP Dec 4, 2018

Mind that the type parameters of the Fn traits are not a stable part of Rust, so be careful not to imply that this should work

This comment has been minimized.

@eddyb

eddyb Dec 4, 2018

Member

This should probably be written as impl FnOnce() (cc @nikomatsakis)

@ExpHP

This comment has been minimized.

ExpHP commented Dec 4, 2018

Something like this which allows omitting the types of the closed over locals could be huge for macros that need to codegen implementors of a trait.

Then again, the feature might also even eliminate the need for most such macros!

let foo = move impl FnOnce<()> {
type Output = ();
extern "rust-call" fn call_once(self, args: ()) {

This comment has been minimized.

@eddyb

eddyb Dec 4, 2018

Member

Aaaa I keep being reminded I need to fix this (it should ideally be just fn call_once(self) here).

@mark-i-m

This comment has been minimized.

Contributor

mark-i-m commented Dec 4, 2018

Question: Does this work?

const FOO: impl Foo = impl Foo { ... };
@burdges

This comment has been minimized.

burdges commented Dec 4, 2018

I believe the syntax by @eddyb makes more sense so long as you must still write out self arguments.

I prefer anonymous types being explicit though, so maybe some syntax involving ||? If I understand, you want closure style capture while creating a value, so maybe struct || impl Trait { ... } but I still do not see self working.

You could name the struct but infer the contents. In fact, if you only have type inference for struct fields, then

fn foo() -> impl Deref<Target=Bar>+DerefMut {
    ...
    #[derive(Clone)]
    struct Ret { x: _, y: _ };  // Or maybe struct Ret ||; 
    impl Deref for Ret {
        type Tagert = Bar;
        fn deref(&self) { self.x }
    }
    impl DerefMut for Ret {
        fn deref_mut(&mut self) { self.y }
    }
    Ret {x,y} // Or maybe Ret |x,y|;
}

In this, all captures take the form self.x, and all captured values must be explicitly passed, using field puns.

We could also improve the mechanisms for creating such types from procedural macros, so like some macro driven || Trait syntax

|| Iterator {
    type Item = Foo;
    fn next() -> Option<Foo> {  // self omitted since some macro processes this 
        ...
    }
}

There is also the FnBorrow traits suggestion which captures many use cases here, with the only strange syntax being some new named lifetime.

Centril and others added some commits Dec 5, 2018

Update text/0000-impl-trait-expressions.md
Co-Authored-By: canndrew <shum@canndrew.org>
Update text/0000-impl-trait-expressions.md
Co-Authored-By: canndrew <shum@canndrew.org>
@jdahlstrom

This comment has been minimized.

jdahlstrom commented Dec 6, 2018

Yeah, this is basically exactly Java's anonymous inner classes. Somewhat amusing that for 20 years Java had closures but only using the verbose AIC syntax. Then they finally added a terse lambda syntax as a syntactic sugar (conceptually if not implementation-wise) . And now in Rust we first had lambdas and now entertain the thought of generalizing them :)

@mikeyhew

This comment has been minimized.

mikeyhew commented Dec 6, 2018

It would be nice to see more examples where the trait is not one of the Fn* traits. The one showing how closures desugar to this syntax is useful as a sort of reference, but examples of new things you could do would be more motivating in my opinion.

@burdges

This comment has been minimized.

burdges commented Dec 7, 2018

Rust closures admit no polymorphism, not even lifetime polymorphism.

I suggested FnBorrow* traits largely because I wanted to call .iter_mut() multiple times, which requires shrinking a lifetime to the receiver's lifetime, ala FnMut() -> IterMut<'self,T>. In essence FnBorrow* would be this proposal, but far more ergonomic, and restricted to Borrow/BorrowMut, AsRef/AsMut, or Deref/DerrefMut`.

It's less common but you might want to tie a closure return lifetime to an argument lifetime too. At that point, you want full lifetime polymorphism for closures.

I think this proposal addresses roughly the same problems as polymorphism for closures. It gains some ergonomics in type descriptions via existing traits, but only with a dramatic ergonomics sacrifice for instantiation.

@Centril

This comment has been minimized.

Contributor

Centril commented Dec 7, 2018

@burdges

Rust closures admit no polymorphism, not even lifetime polymorphism.

fn foo() {
    rank_2(|x: &u8| -> &u8 { x });
}

fn rank_2(x: impl for<'a> Fn(&'a u8) -> &'a u8) {}
@rodrimati1992

This comment has been minimized.

rodrimati1992 commented Dec 7, 2018

for< > could be reused to declare closures like this:

use std::iter;

let cyclic=for<I:Iterator+Clone> |i:I|{
     i.cycle()
};


let list_1=cyclic(vec![1,2,3].into_iter())
    .take(9)
    .collect::<Vec<_>>();

assert_eq!(
    list_1,
    vec![1,2,3,1,2,3,1,2,3]
);


let list_2=cyclic(10..=12)
    .take(9)
    .collect::<Vec<_>>();

assert_eq!(
    list_2,
    vec![10,11,12,10,11,12,10,11,12]
);
@burdges

This comment was marked as off-topic.

burdges commented Dec 7, 2018

Oops thanks :) It's only the receiver lifetime that's unreachable then.

@eddyb

This comment was marked as off-topic.

Member

eddyb commented Dec 7, 2018

Oops thanks :) It's only the receiver lifetime that's unreachable then.

Yes and that's from the traits, not the closures. I think with type Output<'a>; and fn call(&'a self, ...) -> Self::Output<'a>;, that usecase would "just work".
(cc @nikomatsakis I'm not sure how close the borrow-checker is to supporting this)

@burdges

This comment was marked as off-topic.

burdges commented Dec 7, 2018

How would Fn* traits look? I suppose the suggestion of a parallel FnBorrowMut and FnBorrow hierarchy is based on a miss-understanding, but maybe constraining Self in FnOnce::call_once works:

pub trait FnOnce<Args<'i>> {
    type Output<'o>;
    extern "rust-call" fn call_once<'s>(self, args: Args<'s>) -> Self::Output<'s> where Self: 's;
}
pub trait FnMut<Args<'i>>: FnOnce<Args<'i>> {
    extern "rust-call" fn call_mut<'s>(&'s mut self, args: Args<'s>) -> Self::Output<'s>;
}
pub trait Fn<Args<'i>>: FnMut<Args<'i>> {
    extern "rust-call" fn call<'s>(&'s self, args: Args<'s>) -> Self::Output<'s>;
}

impl<F> FnOnce<Args> for F where F: FnMut<Args> {
    extern "rust-call" fn call_once<'s>(self, args: Args<'s>) -> Self::Output<'s> where Self: 's {
        self.call_mut(args)
    }
}
impl<F> FnMut<Args> for F where F: Fn<Args> {
    extern "rust-call" fn call_mut<'s>(&'s mut self, args: Args<'s>) -> Self::Output<'s> {
        self.call(args)
    }
}

I'm still dubious this first impl makes sense. We'd need to inline it before borrow checking like a macro, right?

Also, there are no plans to support ATCs as type arguments like this, right? Could Args instead have an associated lifetime?

pub trait ArgsLifetime { type 'args; }

pub trait FnOnce<Args: ArgsLifetime> {
    type Output<'o>;
    extern "rust-call" fn call_once<'s>(self, args: Args) -> Self::Output<'s>
    where Self: 's, Args::'args: 's;
}
pub trait FnMut<Args>: FnOnce<Args> {
    extern "rust-call" fn call_mut<'s>(&'s mut self, args: Args) -> Self::Output<'s>
    where Args::'args: 's;
}
pub trait Fn<Args>: FnMut<Args> {
    extern "rust-call" fn call<'s>(&'s self, args: Args<'s>) -> Self::Output<'s>
    where Args::'args: 's;
}

impl<F> FnOnce<Args> for F where F: FnMut<Args> {
    extern "rust-call" fn call_once<'s>(self, args: Args) -> Self::Output<'s>
    where Self: 's, Args::'args: 's {
        self.call_mut(args)
    }
}
impl<F> FnMut<Args> for F where F: Fn<Args> {
    extern "rust-call" fn call_mut<'s>(&'s mut self, args: Args<'s>) -> Self::Output<'s>
    where Args::'args: 's {
        self.call(args)
    }
}

We'd still have the syntactic problem of naming this lifetime 's in Fn traits.

In the context of this RFC, if this worked then Fn* traits become far more powerful than AsRef/AsMut, IntoIterator, etc. because tricks like foo(|| x.lock()) or bar(|| y.iter_mut()) work if you write

fn foo<F>(f: F) where F: Fn() -> impl Deref<Target=..>+DerefMut { .. }
fn bar<F>(f: F) where F: Fn() -> impl Iterator<Target=..> { .. }
@eddyb

This comment was marked as off-topic.

Member

eddyb commented Dec 7, 2018

You don't need to do anything for arguments, e.g. for<'a> Fn(&'a T) -> &'a U already exists.

type Output<'a>; only adds one more lifetime the output can depend on, on top of, lifetimes in the argument types and in Self.

@burdges

This comment was marked as off-topic.

burdges commented Dec 7, 2018

I'd think that, if you add only Output<'a>, then Fn(&'a T) -> &'self U can be expressed, but not Fn(&'self T) -> &'self U. In particular, you cannot do

foo(xs.iter_mut().map(|x| [x,&mut y]));

because the 'a in [&'a T] must be either the closure call's 'self for &mut y or else 'x for the x: &'x mut T. We'd want the closure calls to shrink them to agree, so its Args needs to depend on 'self. Right?

@eddyb

This comment was marked as off-topic.

Member

eddyb commented Dec 7, 2018

You can shorten any lifetime parameters of the impl (e.g. 'x) to what you call 'self.
So you'd just have type Output<'a> = [&'a T; 2];.

@Centril

This comment has been minimized.

Contributor

Centril commented Dec 7, 2018

This line of discussion seems increasingly off-topic...

@bill-myers

This comment has been minimized.

bill-myers commented Dec 8, 2018

I think this should support implementing multiple traits (even if they have items with the same signature), and also support deriving traits.

@rodrimati1992

This comment has been minimized.

rodrimati1992 commented Dec 9, 2018

How does this handle implementing a trait with supertraits?For example implementing std::iter::DoubleEndedIterator.

@H2CO3

This comment has been minimized.

H2CO3 commented Dec 12, 2018

I see very little value in this. The example in the RFC is trivial to rewrite as

let world = "world";
let says_hello_world = {
    struct SaysHello<'a>(&'a str);
    impl<'a> fmt::Display for SaysHello<'a> {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "hello {}", self.0)
        }
    }
    SaysHello(world)
};
println!("{}", says_hello_world);

As others have already mentioned it, this is very similar to a lambda. Lambdas are great; however, the use-case of this one is so narrow and so specific that having to do the above transformation by hand once in a lifetime clearly doesn't justify the added language complexity.

@ExpHP

This comment has been minimized.

ExpHP commented Dec 14, 2018

Lambdas are great; however, the use-case of this one is so narrow and so specific that having to do the above transformation by hand once in a lifetime clearly doesn't justify the added language complexity.

  1. This can be highly obnoxious. You need to redeclare all type parameters (named differently, because Rust forbids them to shadow!), duplicate all of the where bounds in scope, name the types of everything in its context, and at the end of all that you gotta construct it.

    • A trailing expression has to go at the end of a function, but the struct item declaration seems out of place unless it is above the impl. Thus: crap, gotta look way up there at the struct def so I can construct it properly

    • A lot of the fields in the constructor are bound to be barely inches away from being field-name punnable:

      Visitor {
          items: &items,
          name: &name,
      }
    • If you're upgrading an existing closure to a trait with more methods, now all of the upvars in your closure that were captured by reference need to be replaced with actual references. This changes the types in your code and can be a pain. match sugar and std library trait impls on & types help somewhat.

    • Naming the types of all the fields isn't always even possible.

  2. APIs in Rust are currently forced to make a significant choice between ergonomics and power. If you accept F: FnMut, then you can only accept fns and closures, because the Fn traits can't be implemented manually. If you take F: MyTrait to allow nameable implementors (or e.g. to simulate for<T: Trait> Fn(&T) using a trait with a generic associated method), all user code now needs to write manual impls.

    • good luck writing a macro that can automate this to any comfortable degree for functions with context!
  3. I simply don't see how the use case is "narrow and specific" (unless you refer specifically to the limitations of this proposal)? I can think of all sorts of places that commonly require this pattern:

    • AST traversal.

    • My code uses them to implement chemical potentials; returning a trait is like returning a bag of closures that can share data (e.g. for caching purposes/incremental computation. Computing these functions is expensive).

    • Any time a function wants to accept multiple callbacks that can be mutably closed over the same data (so multiple FnMuts won't do), e.g. for reporting various types of events that occur during an algorithm:

      struct LongThingVisitor<W>(W, BTreeMap<Id, ProgressBar>);
      
      impl<W: io::Write> LongThingVisitor for W {
          fn item_began(&mut self, item: &Item, id: Id) {
              unimplemented!("add to a list of fancy progress bars")
          }
      
          fn item_progress(&mut self, id: Id, progress: f64) {
              unimplemented!("update a progress bar")
          }
      
          fn item_finished(&mut self, id: Id) {
              unimplemented!("remove from the list")
          }
      }
@H2CO3

This comment has been minimized.

H2CO3 commented Dec 14, 2018

A lot of the fields in the constructor are bound to be barely inches away from being field-name punnable:

That's a moot point, you can use a tuple struct.

APIs in Rust are currently forced to make a significant choice between ergonomics and power.

This is simply false. This would mean that Rust is simply a pain to write in, a viewpoint from which I beg to differ.

If you accept F: FnMut, then you can only accept fns and closures, because the Fn traits can't be implemented manually.

They are unstable. I would be highly in favor of stabilizing manual impl Fn for T in order to explicitly construct non-lambda callable types.

I simply don't see how the use case is "narrow and specific".

It's narrow and specific because it replicates a small subset of functionality already offered by other parts of the language, but only for one concrete situation/style. I understand that there are situations that it might be useful. But "there are situations where it's useful" isn't at all sufficient justification for any feature to be added to the language.

returning a trait is like returning a bag of closures that can share data

In your example, this doesn't really require such impl Trait expressions, only that impl Trait be allowed as the type of a binding if you don't want to allow the specific types to be relied on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment