Skip to content

RFC: partial turbofish #2176

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

Closed
wants to merge 7 commits into from
Closed

Conversation

Centril
Copy link
Contributor

@Centril Centril commented Oct 16, 2017

Rendered.

In concrete terms, this RFC entails that if turbo::<u32, _, _, _>() and Turbo::<u32, _>::new() typechecks, then turbo::<u32>() and Turbo::<u32>::new() must as well.

@Centril
Copy link
Contributor Author

Centril commented Oct 16, 2017

ping @arielb1, @scottmcm, @withoutboats

@eddyb
Copy link
Member

eddyb commented Oct 16, 2017

Oh, now that I see the description, I'm pretty sure this is just #1196, which was closed.

@Centril
Copy link
Contributor Author

Centril commented Oct 16, 2017

@eddyb Seems so, I'll read that one plot ahead.

@Centril
Copy link
Contributor Author

Centril commented Oct 16, 2017

After reading, closing this and re-opening that one would be fine for me.
If those involved in #1196 want to continue on with this one, that is also fine.

@est31
Copy link
Member

est31 commented Oct 16, 2017

I'd love if the feature were opt in, as in you have to do turbo::<u32, ..>() instead of turbo::<u32, _, _, _>(). This would be consistent to other parts of the language like tuple patterns.

@Centril
Copy link
Contributor Author

Centril commented Oct 16, 2017

@est31 If one were to pick, I'm strongly in favour of turbo::<u32>(), but you could also allow both turbo::<u32, ..>() and turbo::<u32>(). To me, turbo::<u32>(), is better wrt. API evolution as discussed in the motivation of #1196.

@est31
Copy link
Member

est31 commented Oct 16, 2017

@Centril I don't think allowing both would be useful, either do turbo::<u32>() or turbo::<u32, ..>().

API evolution is much better solved by doing type defaults on the generic parameters you add, which you can omit already now. I mean when you add a generic param to a function, the type param must have been a concrete type previously. Just add that as default and it works.

On the other hand, if you add a generic param via type inference, some of your code can now silently use a new type, and behaviour might change. Also, the compiler allows breakage in inference from one compiler version to the next.

@scottmcm scottmcm added the T-lang Relevant to the language team, which will review and decide on the RFC. label Oct 17, 2017
@m4b
Copy link

m4b commented Oct 18, 2017

I was literally complaining about this on irc the other day, so +1 for me! I actually can’t +1 this enough.

Forcing users to supply _ for extra generic parameters when the compiler literally already knows it is so beaurocratically draconian it’s hard for me to relate why this is desirable, and as the document notes makes working with complex genetics significantly more burdensome.

Furthermore I just don’t buy readability or “silent usage” concerns; generic parameter defaults already opened that panadoras box imho so any concerns for client side usage of generics (which is really what the turbo fish does) apply equally to generic parameter defaults.

Also on that note:

  1. I agree should def not be both. And I definitely prefer no ..; it should mirror generic defaults which allows their omission, ie I’m in favor of what seems to me the natural behavior of supplying subsequent inferred type arguments to the turbofish.

  2. Generic defaults are not a solution to this problem in general. Sometimes you can’t control the upstream crate. And sometimes a generic default is not appropriate at all. This is about ergonomics when calling a generic function and I think the proposal here nails that aspect.

I have a good example of when inferring the type is very natural and expected behavior (but it’s not, because it doesn’t work that way) I’ll post here when I have some more, but I also think the motivating examples are really great. So yea, great work and I’m hoping a version of this makes it through !

@est31
Copy link
Member

est31 commented Oct 18, 2017

This is about ergonomics when calling a generic function and I think the proposal here nails that aspect.

But overcompensating isn't a solution either. .. improves ergonomics pretty well, while preserving the feature that you know whether there is some inference going on.

@Centril
Copy link
Contributor Author

Centril commented Oct 18, 2017

@m4b While writing turbo::<X, _> is annoying, I wouldn't go as far as to say it's "beaurocratically draconian". That's a bit much.

I actually think the motivation part of the RFC is quite thin on examples. I'd love to have more of them to add from real code and not just my off-the-top-off-my-head examples.

@est31
I don't buy the "This would be consistent to other parts of the language like tuple patterns." argument - while .. is natural for destructuring and patterns, I find it annoying for types and turbofish. I find it much more convincing that what is inferred for types shouldn't be mentioned, and that inference occurs should also be inferred.

In my opinion .. hurts ergonomics since it actually makes it longer for the (probably) most common case, having a single _ at the end.

Unless there is a technical reason why .. is required - for example in relation to default generic type parameters - I see no reason to change the RFC to use .. instead, unless there's a heavy consensus that this is more ergonomic.

@m4b
Copy link

m4b commented Oct 18, 2017

While writing turbo::<X, _> is annoying, I wouldn't go as far as to say it's "beaurocratically draconian". That's a bit much.

Haha sorry, yea sometimes I'm a bit much 😆

This is an example off top of my head I just ran into and which annoyed me, I'll inline it here:

fn disassemble<Function: Fun + DataFlow + Send>(binary: &str) -> Result<Program<Function>> {
    let (mut proj, machine) = loader::load(Path::new(&binary))?;
    let program = proj.code.pop().unwrap();
    let reg = proj.region().clone();
    info!("disassembly thread started");
    Ok(match machine {
        Machine::Avr => analyze::<avr::Avr, Function>(program, reg.clone(), avr::Mcu::atmega103()),
        Machine::Ia32 => analyze::<amd64::Amd64, Function>(program, reg.clone(), amd64::Mode::Protected),
        Machine::Amd64 => analyze::<amd64::Amd64, Function>(program, reg.clone(), amd64::Mode::Long),
    }?)
}

Adding Function as the trailing parameter to analyze is just tedious (beaurocracy ;)); it's already annotated in the function parameter both as a generic and in the return type, and will be reified when called by a client via turbofish, e.g., analyze::<neo::Function>.

Imho, it just looks beautiful, intuitive and awesome without the extra annotation inside the body, in analyze.

I don't buy the "This would be consistent to other parts of the language like tuple patterns." argument - while .. is natural for destructuring and patterns, I find it annoying for types and turbofish. I find it much more convincing that what is inferred for types shouldn't be mentioned, and that inference occurs should also be inferred.

I agree with this completely. I also believe it would add to confusion, as when people will discover generic defaults, they will ask, why are those allowed to be left out when calling a function without a .. diaresis, but turbofish requires ..?

Imho, there's a natural correlation and synergy between omission of generic defaults and omission of inferred types in turbofish, and I personally would look forward to very cool patterns and chains of generics that it would allow.

Anyway, I'm just repeating myself now more or less, besides example, so yea :)

@cristicbz
Copy link

@m4b in you example code, you could just do ::analyze<avr::Avr, _> etc, right? You don't need to spell out function, right?

@m4b
Copy link

m4b commented Oct 18, 2017

Yes, that’s what this issue is about, omitting inferred parameters

EDIT

@cristicbz I just realized are you asking whether it will compile with _ ? If so I don’t know that’s a good question. Seems to me it should obviously infer the type there but haven’t tried

@m4b
Copy link

m4b commented Oct 18, 2017

To make this more concrete it’s likely more parameters will be added to disassemble and analyze, which leads to more _

@Evrey
Copy link

Evrey commented Oct 18, 2017

I have an other reason not to go with x::<T, ..>:

Const generics are coming in the near future. A next possible step is to maybe have variadic generics comparable to C++'s variadic templates. I'd like to see that .. in generics being reserved for that. At least for me, variadic generics would be the first thing coming to my mind when guessing what x::<T,..> is supposed to mean. This also has a nice symmetry with va_list.

An other proposal for those who like it explicit:

We'll have x::<'_, T> for those wanting to infer a lifetime parameter. Why not expand the meaning of _ for "I don't give a damn", so that any x::<T, _, _, _, _, _ /*etc. ...*/> can be written as a mere x::<T, _>. rustc would accept _ here as an: "At least one infered generic argument."

@m4b
Copy link

m4b commented Oct 18, 2017

Also can the name for ::<F, _, _, _>and friends be the turbo sword ?

@est31
Copy link
Member

est31 commented Oct 18, 2017

I also believe it would add to confusion, as when people will discover generic defaults, they will ask, why are those allowed to be left out when calling a function without a .. diaresis, but turbofish requires ..?

There is also a difference. Default type parameters are never inferred. Either way, I'm not a super strong supporter of .. vs omitting the arguments.

An other proposal for those who like it explicit: [...] rustc would accept _ here as an: "At least one infered generic argument."

No, that sounds like a very bad idea, as it would be even more inconsistent with what _ means in other places. In fact I'd prefer if there was a lint for _ trailing parameters if we go with the proposal by @Centril .

@Centril
Copy link
Contributor Author

Centril commented Oct 18, 2017

@Evrey The argument regarding variadic generics seems reasonable.

@m4b And it was called the Turbo Sword until the end of time.

@est31 Linting seems like a great idea.

@withoutboats
Copy link
Contributor

while preserving the feature that you know whether there is some inference going on.

Nearly all types, and especially that includes generic parameters, are inferred already. You have no idea when seeing foo.bar() if bar had some type parameters that have been inferred. What is the significance of presenting this information in this rare case where you're using a turbofish?

@withoutboats
Copy link
Contributor

To me, the downside of this feature is mainly that I might make some bad assumptions: e.g. let's say I see .collect::<Result<Vec<_>>>(). I tend to assume when I see Result with some parameter that there's been a type alias that specifies the error, and I might go looking for it. But in fact it could be that the error type could just be inferred.

But maybe after this feature exists, I would grow used to writing Result<Vec<_>> instead of Result<Vec<_>, _>, and I wouldn't make an assumption like that anymore.

@Centril
Copy link
Contributor Author

Centril commented Oct 18, 2017

@withoutboats This RFC does not propose that you be allowed to write Result<Vec<_>> instead of Result<Vec<_>, _>. It only applies to function application and not _ in type constructors.

@Centril
Copy link
Contributor Author

Centril commented Oct 18, 2017

Correction: This RFC also applies to _ in type constructors, but only when also part of turbofish (which implies function application).

In other words, it applies to:

Turbo::<Vec<u32>, _>::new(1, 1);
// becomes:
Turbo::<Vec<u32>>::new(1, 1);

but not:

let x : Turbo<Vec<u32>, _> = expr;
// will not become:
let x : Turbo<Vec<u32>> = expr;

@Centril
Copy link
Contributor Author

Centril commented Oct 25, 2017

@Evrey Note: as the RFC is currently written: x::<T, _, _, _, _, _ /*etc. ...*/> can be rewritten as x::<T> or x::<T, _> or x::<T, _, _> or x::<T, _, _, _> and so on.

@bluss
Copy link
Member

bluss commented Oct 28, 2017

I have a concern — how does it work with default type parameter fallback? (the tracking issue is rust-lang/rust/issues/27336).

@Centril
Copy link
Contributor Author

Centril commented Nov 3, 2017

@bluss So; Repeating what I said on IRC a while back:

Since this RFC is mostly about a syntactic transformation that can be run before type inference, the idea is that if any function or type has default type parameter fallback, and inference is not otherwise constrained, then the expanded _ in a certain position, will use default type parameter.

An example: Say we have a function foo with 5 type parameters, 3 of which are defaulted. The 2 first parameters are specified in turbofish. The 3rd is constrained to be non-default and not specified in turbofish. The 2 last parameters are defaulted and not specified in turbofish. The compiler will expand this call foo::<A, B>(..) to: foo::<A, B, _, _, _>(..). Since this transformation done purely on the length of type parameter list by inserting missing _s, then type inference can replace _ in a given position with the default, or some other type that must be used.

If this sounds reasonable, I'll add a note about this in the RFC.

@Centril
Copy link
Contributor Author

Centril commented Dec 30, 2017

Ping @bluss on my last comment ^

leoyvens added a commit to leoyvens/rfcs that referenced this pull request Feb 3, 2018
`$($tparam: ident),*`. If while calling `turbo::< $($tconcrete: ty),* >(...)` a
suffix of the applied types can be replaced with a list of `_`s of equal length,
then the suffix may be omitted entirely. A shorter suffix may be chosen at will.
This also applies to *turbofish*ing types (structs, enums, ..), i.e:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This * in the middle of the word seems to be messing up GH's markdown -- at least in the preview. Can you remove it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed =)

@scottmcm
Copy link
Member

scottmcm commented Mar 1, 2018

In C++ I certainly appreciate being able to do things like castlike_thing<T>(x) even though that function takes two parameters. But that implies things like the return type should always be the first generic argument, which already isn't the case on things like transmute...

@joshtriplett
Copy link
Member

joshtriplett commented Mar 1, 2018

We discussed this in the lang team meeting. There was some concern about the degree of implicitness. There might be some merit to the proposal of opting into this in the type definition (e.g. defining fn foo<T, U = _, V = _> to allow foo::<SomeType> without the , _, _), though there wasn't consensus there; at a minimum, though, that should likely be included as an alternative. But there was not consensus that this RFC should be accepted. We also had concerns about interactions with default type parameters and about consistency between turbofish and other instances where type parameters appear.

We'll follow up with additional next steps.

@Centril
Copy link
Contributor Author

Centril commented Mar 1, 2018

@joshtriplett

We also had concerns about interactions with default type parameters

Please elaborate on this part since I have no idea what the problem could be...
My understanding is that filling in _s would be done before inference, and that defaults would affect how type inference behaves by providing fallbacks.

There might be some merit to the proposal of opting into this in the type definition

I'm happy to include any and all alternatives; I've included some text about this possibility and what I think about it; A summary of my initial views are:

With respect to requiring fn foo<T, U = _, V = _> that seems to penalize default position (I assume it is, but I could be wrong..) that = _ is right in most cases. Granted, opting into partial turbofish is better than no partial turbofish at all, but still... I'm curious to know what the use cases for not wanting partial turbofish are? Also, I believe that opt-in will lead to a less consistent experience for library users as they will not have to check documentation to see if a function or data constructor has opted in or not - I believe this uncertainty to be a drawback.

We also had concerns about interactions with default type parameters and about consistency between turbofish and other instances where type parameters appear.

I assume this refers to not allowing Vec instead of Vec<_> and Result<Vec> instead of Result<Vec<_>, _>? I'm open to that, but we'd have to think carefully about the interactions with possible higher kinded types in the future.

@scottmcm
Copy link
Member

scottmcm commented Mar 2, 2018

default type parameters

I think the concern is that it makesMyType<T> in the function signature mean something different from MyType::<T> in an expression path, if the former is defaulting and the latter is inferring.

AFAIK having infers and defaults simultaneously is really hard, or at least we don't have a good implementation strategy for it at this time. (Similar to the troubles with coercions, iirc.)

@joshtriplett
Copy link
Member

I assume this refers to not allowing Vec instead of Vec<> and Result instead of Result<Vec<>, _>? I'm open to that, but we'd have to think carefully about the interactions with possible higher kinded types in the future.

No, this referred to allowing Type<Foo> instead of Type<Foo, _, _>.

@petrochenkov
Copy link
Contributor

@scottmcm

I think the concern is that it makes MyType<T> in the function signature mean something different from MyType::<T> in an expression path, if the former is defaulting and the latter is inferring.

That's exactly the current rules extended to more cases, missing generic arguments are already defaulted in "type context" including signatures and inferred in "value contexts" (#2176 (comment)).

@Centril
Copy link
Contributor Author

Centril commented Mar 3, 2018

@scottmcm

I think the concern is that it makes MyType<T> in the function signature mean something different from MyType::<T> in an expression path, if the former is defaulting and the latter is inferring.

I'm confused.. This RFC does not affect inference or defaulting at all.

All it would do in this particular context is that you'd be able to write:

fn main() {
    struct MyType;
    struct Foo<T, U>(T, U);
    Foo::<u32>(1, MyType);
}

If you have a problem in mind, can you illustrate with an example?

@joshtriplett

No, this referred to allowing Type<Foo> instead of Type<Foo, _, _>.

But this RFC does not currently propose that you should be able to write Type<Foo> instead of Type<Foo, _, _>... Can you elaborate on the problem you see?

@nikomatsakis
Copy link
Contributor

I didn't see anybody bring it up yet, but I see a connection between this RFC and impl Trait. Presently, we have a limitation such that in a function that uses both explicit type parameters and impl Trait, you cannot use the turbofish operator:

fn foo<T>(x: impl Debug) { }

...

foo::<u32>(...) // error

This restriction came out of a conversation between @petrochenkov and I where it was apparent that we had different expectations, so we decided to postpone the problem. In particular, my expectation is that I would want to write code above, where impl Debug is not treated as a type parameter that a user can manually specify -- those ought to be written out distinctly.

@petrochenkov, in contrast, was arguing that there ought to be some sort of transition path for a function that is using type parameters today, so that it can use impl Trait without disturbing existing users who may be using turbofish (obviously this would only sometimes apply):

fn bar<T: Debug>(x: T) { .. } // today
fn bar(x: impl Debug) { .. } // tomorrow

bar::<u32>(...) // continues to work

Personally, I still feel that impl Trait parameters ought not to be specifiable via turbofish: but it's interesting that even if we did allow them to be, this RFC would enable you to ignore them as a user if you wanted.

@nikomatsakis
Copy link
Contributor

I have to say that I personally am becoming somewhat fond of the opt-in variant of this RFC:

// the `_` means `T` can be elided at the call site:
fn foo<T = _>(...)

But in general I feel like there is a bit of a 'morass' of things I would like to see more harmonized:

The T = _ syntax can definitely be viewed as a special case of today's defaults on types: if you specify some of the type parameters, the extra ones are defaulted always, but they happen to be defaulted to an inference variable. (We could permit this on types too, actually, it would just only apply in struct literals and other "expression" contexts.)

However, in @leodasvacas's RFC, T = _ indicates a type parameter whose default is implied by the types of the parameters of the function. That is not the case here. So we start to see a bit of conflict (though perhaps a resolveable one).

I have been historically quite grumpy with how our defaults in type parameters in any case. In particular, I don't like them being a linear list. I want to give names. Consider HashMap:

pub struct HashMap<K, V, S = RandomState>

What happens if we want to add an allocator parameter (say) to HashMap? Now, in order to specify the allocator, I have to specify the S parameter too? That seems dumb. This now seems sort of connected to optional parameters in functions, of course.

One complication here is that Foo<Bar=Baz> already has a meaning, as an associated type binding, so we would probably want to be careful enabling some syntax like HashMap<Allocator = Foo> (particularly since trait type parameters probably want defaults too). Or maybe it's ok for the meaning of that to depend on whether Allocator is declared as an named optional type parameter or an associated type.

@glaebhoerl
Copy link
Contributor

I have to say that I personally am becoming somewhat fond of the opt-in variant of this RFC:

// the `_` means `T` can be elided at the call site:
fn foo<T = _>(...)

What situation is there where, as a library author, you would not want to allow this? Would it be remotely common? I'm concerned that this would lead to every generic function in every library ever being written with T = _ parameters in order to be feature complete and enable the best experience for their users (or else have it be considered an API bug), so we would have pushed more work onto library authors and created more noise for little gain.

@Centril
Copy link
Contributor Author

Centril commented Mar 4, 2018

@nikomatsakis

Personally, I still feel that impl Trait parameters ought not to be specifiable via turbofish

This is my view also; but I hadn't thought of partial turbofish in this light. Thanks for bringing it to my attention.

I have to say that I personally am becoming somewhat fond of the opt-in variant of this RFC:

Here I second @glaebhoerl's point about use cases for not opt-ing in.. are there such notable cases?

(Analogous, though distinct) the desire for optional parameters in functions

I believe that if we re-imagine turbofish application site (what is inside < and > in ::<..>) as a place for implicit arguments we could support optional parameters nicely and consistently by having runtime values with defaults as in:

fn foo<x: usize = 42>() {
    println!("{}", x);
}

This is also a nice path forward if we want to pursue full dependent types.

Inference fallback such as @leodasvacas proposed in #2321
[...] However, in @leodasvacas's RFC, T = _ indicates a type parameter whose default is implied by the types of the parameters of the function. That is not the case here.

I assume this does not apply to the main proposal of this RFC to not permit opt-in and always allow omitting extra , _ whether the definition site wants to or not. AFAIK, this RFC and #2321 are fully compatible in the sense that this RFC has no effect on inference, it just mechanically inserts , _s and lets inference take on from there. The intersection of this RFC and #2321 is that the latter simply adds more cases where , _ is permissible, and so those can be elided with this RFC.

In particular, I don't like them being a linear list.

For now, I think we should improve what we can and solve the real world problems caused by having to add , _ and not having defaults.

I think that while linear lists scale very poorly, there are few cases where you have a lot of parameters that it matters not whether the linearity scales poorly or not. Tho, the HashMap example you bring up is showing in just how badly things scale. You could mitigate it with a type alias that reorders parameters or fixes to RandomState.

To solve this issue, I think we need changes or additions that are much larger than either #2321 or this RFC proposes. A possible syntax could be:

pub struct HashMap<K, V, S = RandomState, A = Heap> { .. }

let foo: HashMap<K, V, {A} = OtherAlloc> = HashMap::default();
// variations with different sigils:
let foo: HashMap<K, V, [A] = OtherAlloc> = HashMap::default();
let foo: HashMap<K, V, |A| = OtherAlloc> = HashMap::default();
let foo: HashMap<K, V, #A = OtherAlloc> = HashMap::default();

Here, {A} refers to name A as used in HashMap<K, ..> and I think it looks pretty good.

If we can infer K, V then we can also write:

let foo: HashMap<{A} = OtherAlloc> = HashMap::default();
// ..

@petrochenkov
Copy link
Contributor

petrochenkov commented Mar 4, 2018

@nikomatsakis

I didn't see anybody bring it up yet, but I see a connection between this RFC and impl Trait.

Data point - how C++ does this:

template<typename T, typename U>
void f_long(T x, U y) {}

// Shortcut for the long form, can be `auto` (aka `impl Anyhing`) or `Concept` (aka `impl Trait`)
void f_short(auto x, auto y) {}

int main() {
    f_long<int, int>(0, 1); // Both are explicitly specified
    f_long<int>(0, 1); // One is explicitly specified, another is inferred
    f_long(0, 1); // Both are inferred
    
    f_short<int, int>(0, 1); // Both are explicitly specified
    f_short<int>(0, 1); // One is explicitly specified, another is inferred
    f_short(0, 1); // Both are inferred
}

http://coliru.stacked-crooked.com/a/2cea20ce31e64e54

I.e. arguments for parameters defined with "impl Trait" can be provided explicitly and trailing parameters are inferred if not provided, so the short form is simply a sugar for the long form.

@nikomatsakis
Copy link
Contributor

@glaebhoerl

What situation is there where, as a library author, you would not want to allow this

Hmm, a fair question. =) I had imagined you would only want to allow it in cases where you expect turbofish to be used -- i.e., if there is a kind of "primary" type parameter that you expect users to have to specify. But I admit I can't think of a strong reason not to permit it.

@comex
Copy link

comex commented Mar 5, 2018

HashMap<K, V, A: OtherAlloc>?

Might be easy to confuse with trait bounds, but that’s not so different from the similarity between a struct declaration, struct Foo { a: T } and the syntax for a value, Foo { a: val }.

@Centril
Copy link
Contributor Author

Centril commented Mar 5, 2018

@nikomatsakis, @comex I've started a discussion about named type parameters here: https://internals.rust-lang.org/t/named-type-parameters/6921

@aturon
Copy link
Member

aturon commented Mar 15, 2018

Note that @Centril, @leodasvacas and some others are now taking up discussion related to default type parameters on functions, turbofish, and more -- hopefully resulting in a new RFC. As such, I'm going to close this one for the time being.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.