Skip to content

RFC: Consider supporting function return type deduction #10448

Description

@erickt

I had a brief chat on IRC today about adding support for deducing the function return type. I realize we decided against doing this a couple years ago, but I think it's worth revisiting in light of iterators. One of the challenges with our iterators is that the types of complex iterators are unwieldy. For example, here's the type of chaining two vecs together:

type T<'self> = std::iter::Chain<
    std::vec::VecIterator<'self, int>,
    std::vec::VecIterator<'self, int>
>;

fn main() {
    let x = [1,2,3];
    let y = [4,5,6];
    let iter: T = x.iter().chain(y.iter());
}

While we can generally rely on type inference inside a function, things get really messy once we try to return an iterator from a function. One way to avoid this complicated type is to box of the iterators inside a ~Iterator<int>, but according to @thestinger, this is ~1000x slower than with a monomorphized iterator. Having return type deduction would make it much easier to pass around iterators.

This would also be really handy for macros and syntax extensions. For example, we could convert the serialize.rs over to iterators without needing generators (#9145).

This has been proposed for (C++14)[http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3638.html].

@cmr had an argument against this. He felt that adding return type deduction would make it that much easier to break ABIs.

Activity

  1. thestinger commented on Nov 12, 2013

    @thestinger
    Contributor

    FWIW, it's only 1000x slower with a vector iterator and is more reasonable with something like an unbuffered line iterator

  2. erickt commented on Nov 12, 2013

    @erickt
    ContributorAuthor

    @huonw mentioned that typeof (#3228) could be used to address the complication of code generating a complex return type. Assuming that typeof used as the return type would have a method's self in scope, you could write:

    struct Foo { x: ~[int] }
    
    impl Foo {
        fn foo(&self) -> typeof(self.x.iter()) { self.x.iter() }
    }
    

    It would be uncomfortable enough for a human to write so it wouldn't get much use, but it'd be simple for a macro/syntax extension to duplicate the fn body into both the type and the actual fn body.

  3. thestinger commented on Nov 12, 2013

    @thestinger
    Contributor

    C++11 added decltype, but C++14 is adding decltype(auto), in part to solve the issue of still not being able to return unboxed closures with decltype and to avoid needless repetition of the function body.

  4. huonw commented on Nov 12, 2013

    @huonw
    Contributor

    FWIW, Rust has nicer macros than C++, so it is possible to write something like

    ret_type_infer!(fn foo(x: int, y: int) -> _ { <body> })
    
    // expands to
    
    fn foo(x: int, y: int) -> typeof({ <body> }) { <body> }

    (For this, typeof would have to understand return.)

  5. thestinger commented on Nov 12, 2013

    @thestinger
    Contributor

    It still wouldn't work for unboxed closures.

  6. huonw commented on Nov 12, 2013

    @huonw
    Contributor

    It could, in theory. If the type of an unboxed closure depended only on the AST/types (ignoring NodeIds) used to construct it (i.e. its body and its captures), so two equal pieces of code would give the same closure type.

    (This is likely to be unreasonably complicated to achieve, of course.)

  7. nikomatsakis commented on Nov 13, 2013

    @nikomatsakis
    Contributor

    Adding typeof is in no way simpler than just inferring the return type in the first place. I agree that as types get more complicated, writing out the return type is sometimes unreasonable. There is no fundamental reason we cannot infer return types so long as the call graph in the file forms a DAG; it complicates type checking, in that the current two-phase structure no longer works. Rather, what we call collect and check must be intertwined. (We can even infer return types in the face of cycles in the call graph, but it gets more complicated, because we would need to use a single inference context for multiple fn items.) Still, it's not that hard. Future efforts to parallelize would get more complicated but also not impossible; something like futures models the problem fairly well.

  8. emberian commented on Nov 13, 2013

    @emberian
    Contributor

    OTOH, I wouldn't be opposed to ABI-breaking features if it wasn't allowed to mark them #[stable], and if there was a lint for using things which either make breaking ABI unnoticable .

  9. pnkfelix commented on Mar 13, 2014

    @pnkfelix
    Contributor

    I suspect associated items could alleviate some of these cases, though probably not all of them.

    E.g. instead of writing:

    fn foo<'a>(v: &'a [int]) -> std::iter::Rev<std::vec::Items<'a, int>> {
        v.iter().rev()
    }

    (which require you have knowledge of the names of implementing structs within std::vec and also std::iter), with associated types you would instead write something like

    fn foo<'a>(v: &'a [int]) -> std::vec::Slice<'a, int>::Iter::Rev {
        v.iter().rev()
    }

    and in practice since you'll be use'ing the types you're working with, that's likely to be just:

    fn foo<'a>(v: &'a [int]) -> Slice<'a, int>::Iter::Rev {
        v.iter().rev()
    }

    Like I said at the outset, it doesn't handle all cases cleanly (e.g. the Chain example still looks bad I think), but it would enable a step forward in terms of making it easy to write these signatures without much thought. (And also write methods like foo in generic code since it does not need to encode the concrete iterator types.)


    I actually suspect the ideal thing could be a hypothetical statically-resolved existential syntax like

    fn foo<'a>(v: &'a [int]) -> <I:Iter> I {
        v.iter().rev()
    }

    that is the dual to the universal parameterization syntax that one sees with <I:Iter> on the left-hand side of the ->; in this case the compiler would enforce that clients of foo never used any methods on I except for those provided by the Iter interface, but the method dispatch would still be tied via monomorphization directly to the implementing methods provided by I itself, not dynamically dispatched the way you would see with a trait object like ~Iter. (It would still break ABI's, but at least you'd have some notion of source compatibility with the above interface.)

    But no need to work on any of this until after 1.0 I think.

  10. glaebhoerl commented on Mar 13, 2014

    @glaebhoerl
    Contributor

    @pnkfelix That's #11455. :)

  11. pnkfelix commented on Mar 13, 2014

    @pnkfelix
    Contributor

    @glaebhoerl ah, thanks for the link. I suppose my suggestion is indeed a kind of type-inference, though my mental model remains that of a statically-resolved existential (probably because I see the <I:Iter> as denoting either forall or exists depending on which side of the -> arrow it is on. In any case, I do find it interesting that none of the syntaxes on #11455 took the form of an existential with an explicitly bound name, which could be useful for doing constructions like:

    fn bar<'a>(v: &'a [int]) -> <I:Iter> Option<I> { ... }
    
  12. glaebhoerl commented on Mar 14, 2014

    @glaebhoerl
    Contributor

    @pnkfelix The acid test here is:

    fn f(n: int) -> Trait { ... }
    let a = f(0);
    let b = f(1);
    swap(&mut a, &mut b);
    

    Should we allow this? We know the result of f(n) can only ever be the same type, so we could allow it: in this case, it's better to think of it as inference. However, if we think of it as an existential, then we should assume that f(n) might branch on n and return different types for different inputs (even though, physically speaking, it cannot), and therefore we should reject the above.

    I've actually come around to prefer the latter: this means that from outside f(), it will behave exactly like an unboxed trait object, which is good for intuition and consistency. It's also the forwards-compatible choice: we can make it more permissive later if we choose to without breaking anything, but not vice versa.

    In any case, I do find it interesting that none of the syntaxes on #11455 took the form of an existential with an explicitly bound name, which could be useful for doing constructions like:

    fn bar<'a>(v: &'a [int]) -> <I:Iter> Option<I> { ... }
    

    This reads to me as a universal: forall I: Iter. Option<I>, not an existential. In Rust existentials are formed by omitting the name of a type and substituting the name of a trait. If we want to follow the analogy (and I think we should), then we should write simply Option<Iter>. (If, following the above, from outside of bar() the analogy Iter : &Iter :: T : &T is exact, then I think this "abuse of notation" is safe to introduce.)

    Incidentally:

    fn bar<'a>(v: &'a [int]) -> Option<Iter> { ... }
    

    This happens to be incorrect as written, because information about the lifetime is lost in the result. Just as with boxed trait objects, you would have to write -> Option<Iter: 'a>.

  13. pnkfelix commented on Mar 14, 2014

    @pnkfelix
    Contributor

    From the viewpoint of the caller of bar, I was intending something that denoted fn bar<'a>(v: &'a [int]) -> Exists (I : 'a + Iter) . Option<I>. I thought putting the <I:'a + Iter> on the right-side of the arrow as a way to denote this was cute, but if it is confusing, then obviously we would need to find another syntax.

    Anyway, you are correct, my example did not illustrate an instance where one would need explicit binding of an actual name. One needs binding if one wants to use the same existentially-bound type variable multiple times in the type expression, like in the following, which returns a tuple where both Iters must actually be the same type:

    fn bar<'a>(v: &'a [int]) -> Exists (I : 'a + Iter) . (I, I)
    

    (Is it useful? I don't know yet. I suppose the implicit point of your message is that our existing syntax for existential types a la trait objects does not let one express something like the above, and therefore it is unlikely to be of use.)

  14. glaebhoerl commented on Mar 14, 2014

    @glaebhoerl
    Contributor

    I think my implicit point was that explicit existential syntax, while potentially useful, is orthogonal to this feature.

  15. dobkeratops commented on Apr 10, 2014

    @dobkeratops

    tangential comment,having perhaps -> _ could help IDE autocomplete: where you're trying to compile incomplete code, the 'dot-completion' workflow is generally forward inference where you dig into a datastructure exploring it with each call. I realise the arguments against whole program inference, perhaps you could have this restricted to 'inline' or 'private' functions if you're concerned about that, and of course the IDE issue of trying to compile incomplete code where the return type doesn't yet match up could be solved with compiler flags specific to that case.

  16. aturon commented on Jun 3, 2014

    @aturon
    Contributor

    I've written a formal RFC: rust-lang/rfcs#105

  17. thestinger commented on Sep 19, 2014

    @thestinger
    Contributor

    Keeping around an issue here isn't necessary because this isn't backwards incompatible or actionable. A proposal needs to be accepted through the RFC process.

  18. added a commit that references this issue on Mar 10, 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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions