Skip to content

RFC: implicit generic function arguments with bounds #11196

Description

@eddyb

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.

Activity

  1. lifthrasiir commented on Dec 30, 2013

    @lifthrasiir
    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.

  2. eddyb commented on Dec 30, 2013

    @eddyb
    ContributorAuthor

    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) {...}
  3. SiegeLord commented on Dec 30, 2013

    @SiegeLord
    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))
    }
  4. huonw commented on Dec 30, 2013

    @huonw
    Contributor

    @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.

  5. eddyb commented on Jan 14, 2014

    @eddyb
    ContributorAuthor

    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.

  6. aturon commented on Jun 3, 2014

    @aturon
    Contributor

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

  7. rust-highfive commented on Sep 24, 2014

    @rust-highfive
    Contributor

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

  8. added a commit that references this issue on Jul 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions