Skip to content
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: implicit generic function arguments with bounds #11196

Closed
eddyb opened this issue Dec 29, 2013 · 7 comments
Closed

RFC: implicit generic function arguments with bounds #11196

eddyb opened this issue Dec 29, 2013 · 7 comments

Comments

@eddyb
Copy link
Member

eddyb commented Dec 29, 2013

Using generic functions with trait bounds results in efficient code generation, at the cost of syntactic simplicity (compared to using trait objects).

Consider this snippet:

fn stringify<T: ToStr>(x: T) -> ~str {
    x.to_str()
}
// Making use of a hypothetical Fn trait.
fn map2fn<T, U, V, F1: Fn<T, U>, F2: Fn<U, V>>(x: T, f1: F1, f2: F2) -> V {
    f2(f1(x))
}

Now with trait bounds moved to argument types (creating implicit generic type parameters):

fn stringify(x: ToStr) -> ~str {
    x.to_str()
}
// Making use of a hypothetical Fn trait.
fn map2fn<T, U, V>(x: T, f1: Fn<T, U>, f2: Fn<U, V>) -> V {
    f2(f1(x))
}
// Or closures as sugar for Fn (unboxed!).
fn map2fn<T, U, V>(x: T, f1: |T| -> U, f2: |U| -> V) -> V {
    f2(f1(x))
}

Note that this would be limited to values as references or pointers would conflict with current syntax for trait objects.

However, optimizing &Trait in an argument type (or even a structure field? there is room for discussion here) to a generic (<T: Trait> ... &T) wouldn't cause any serious issues AFAICT.
And it would mean we can make the closure syntax be sugar for &Fn without losing backwards compatibility while allowing compile-time monomorphization.

@lifthrasiir
Copy link
Contributor

Besides from the obvious ABI compatibility issue, this proposal severely limits the user's ability to selectively disable monomorphization. For example, if a function fn f<T: Trait>(x: &T) is big enough and used with multiple different Ts, the user should be able to "refactor" this into fn f(x: &Trait) at expense of virtual method call overheads, in order to get a smaller binary.

@eddyb
Copy link
Member Author

eddyb commented Dec 30, 2013

The proposal is by no means set in stone, but here's a quick way of disabling monomorphization:

fn f_virtual<T: Trait>(x: &T) {
    f(x as &Trait)
}
// Maybe this attribute could disable monomorphization by itself?
// Or we could have another attribute, like #[virtual_trait_args].
#[inline(never)]
fn f(x: &Trait) {...}

@SiegeLord
Copy link
Contributor

I'd rather have a general solution that interacts well with the rest of the language rather than a special case one (note how T, U, V remain where they are!) that doesn't. I'd look at what other languages with traits/concepts/etc do and see if it's any better. E.g. in D it is also more syntactically light to use dynamic dispatch, and yet most use static dispatch anyway. Here is what a complicated function signature with type constraints looks like in D:

void fill(Range1, Range2)(Range1 range, Range2 filler)
    if (isInputRange!Range1
        && (isForwardRange!Range2
            || (isInputRange!Range2 && isInfinite!Range2))
        && is(typeof(Range1.init.front = Range2.init.front)))
{

}

I.e. the type constraints (the code that starts after the if) go after the function name/arguments. C++ also has types and type bounds separate. Maybe that's a palatable solution? We even reserve the where keyword which could be used for the purpose:

fn map2fn<T, U, V, F1, F2>(x: T, f1: F1, f2: F2) -> V
    where<F1: Fn<U, V>, F2: Fn<U, V>>
{
    f2(f1(x))
}

// OR

fn map2fn(x: T, f1: F1, f2: F2) -> V
    where<T, U, V, F1: Fn<U, V>, F2: Fn<U, V>>
{
    f2(f1(x))
}

@huonw
Copy link
Member

huonw commented Dec 30, 2013

@eddyb, re virtualisation, I believe @lifthrasiir is just pointing out that the proposed syntax only works for traits not contained in any pointers, specifically (and most importantly), you can't write fn foo<T: Trait>(x: &T) in short hand as fn foo(x: &Trait) without some other disambiguation.

@eddyb
Copy link
Member Author

eddyb commented Jan 14, 2014

Even without the speculation about &Trait being optimized to <T: Trait> &T, this should still work for the non-reference version, if only to keep the same closure type syntax in some cases, if |..Args| -> Ret becomes sugar for Fn<..Args, Ret>.
Is there any issue with passing a closure unboxed instead of boxed? The closure's body wouldn't actually be copied, since we already pass larger-than-register-sized objects by reference.
And I've just remembered about the awkward lifetime syntax - 'a |..Args| -> Ret - with sugar that becomes &'a |..Args| -> Ret, which is just a &'a Trait.

@aturon
Copy link
Member

aturon commented Jun 3, 2014

I've now written an RFC that includes something like this proposal: rust-lang/rfcs#105

@rust-highfive
Copy link
Collaborator

This issue has been moved to the RFCs repo: rust-lang/rfcs#302

flip1995 pushed a commit to flip1995/rust that referenced this issue Jul 31, 2023
[significant_drop_tightening] Fix rust-lang#11189

Fix rust-lang#11189

```
changelog: FP: [`significant_drop_tightening`]: Consider tuples in drop calls
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants