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

Implement default type parameters in generics. #11217

Merged
merged 1 commit into from
Jan 30, 2014

Conversation

eddyb
Copy link
Member

@eddyb eddyb commented Dec 30, 2013

No description provided.

let _ = S::<'a,int>::new::<f64>(1, 1.0); //~ ERROR expected 0 lifetime parameter(s)
let _: S2 = Trait::new::<int,f64>(1, 1.0); //~ ERROR the trait referenced by this path has 1 type parameter, but 0 type parameters were supplied
let _: S2 = Trait::new::<int,f64>(1, 1.0); //~ ERROR the trait referenced by this needs has 1 type parameter, but 0 type parameters were supplied
Copy link

Choose a reason for hiding this comment

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

typo

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, meant to replace the 'has' (which I changed to 'needs' in the typeck code).

@pcwalton
Copy link
Contributor

This clearly needs discussion at the meeting (which is delayed until the new year because of holidays) but I am tentatively in favor because of custom allocators. However, we need to decide how this contrasts with:

pub struct AHashMap<K,V,A> {
    allocator: A,
    ...
}

pub type HashMap<K,V,A> = AHashMap<K,V,Heap>;

Right now this won't work because you won't be able to write HashMap::new(), but I wonder if we aren't better off fixing that problem, because the inability to call methods through typedefs is a major annoyance for other reasons and it might just end up allowing us to get sugary support for custom allocators.

}

fn main() {
Vec::<int, Heap, bool>::new(); //~ ERROR the impl referenced by this path needs at most 2 type parameters, but 3 type parameters were supplied
Copy link
Member

Choose a reason for hiding this comment

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

This line won't pass make tidy's length checker.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes it will (just checked).
I believe tests are allowed to exceed the normal limit because of placement constraints for those comments.

@huonw
Copy link
Member

huonw commented Dec 30, 2013

Yay! (This would also allow, e.g., customising the hash used by the hashmap, which would allow us to resolve issues like #10586 in a nice way.)

As you said on IRC, this needs rpass tests. Maybe a basic test like the following, along with some more complicated ones:

struct Foo<A=(int, char)> {
    a: A
}

fn default_foo(x: Foo) {
     let (_i, _c): (int, char) = x.a;
     x.bar();
}

impl Foo {
    fn bar(&self) {
        let (_i, _c): (int, char) = self.a;
    }
}

fn main() {
    default_foo(Foo { a: (1, 'a') })
}

@huonw
Copy link
Member

huonw commented Dec 31, 2013

Would it be palatable to add this, but keep it behind a #[feature(default_type_params)] gate for now, so that we don't have to perfect it immediately/can remove it later if necessary?

@alexcrichton
Copy link
Member

I have added this to the next meeting agenda (not that this is required to wait until then).

@brson
Copy link
Contributor

brson commented Jan 11, 2014

Let's discuss at next meeting.

@glaebhoerl
Copy link
Contributor

@eddyb I have the same question as pcwalton: aside from ergonomic issues stemming from current deficiencies of the language, why are typedefs not an adequate solution here? In a sense it's even cleaner because it doesn't require the generic implementation to have any knowledge of the default.

@huonw
Copy link
Member

huonw commented Jan 17, 2014

It's not just the ergonomics of calling things on typedefs, it means we'd need a pile of items & typedefs for every generic data structure everywhere (lots of ...MapAlloc & ...Map and ...SetAlloc & ...Set). And HashMap would be particularly bad:

struct HashMapHashAlloc<K, V, Hash, Alloc> {
    ...
}

type HashMapHash<K, V, Hash> = HashMapHashAlloc<K, V, Hash, LibcAllocator>;
type HashMap<K, V> = HashMapHashAlloc<K, V, SipHash, LibcAllocator>;

@glaebhoerl
Copy link
Contributor

  • Wouldn't the choice of hash function be better solved using newtypes on the key? More or less the same issue came up for the Haskell hashable package, and it's the solution they seem to have converged on (though it doesn't seem to have been implemented yet).
  • I note that, using the scheme from your comment, if you want to be able to manually specify only the allocator, even with default type arguments you'll need a type HashMapAlloc<K, V, Alloc> = HashMap<K, V, SipHash, Alloc> typedef, because only arguments at the end are defaulted.
  • What I'm actually concerned about, beyond not being sure this feature carries its weight, is that if/when we gain HKT, I'm fairly sure we'll also want currying / partial application at the type level, and I don't think the two play well with each other. Besides actual type system issues (which I suspect would exist), they appear to want type parameters to be in the opposite order. With HKT you want "extra" parameters like Alloc to be at the front, so that you can write impl<Alloc, Key> Functor for HashMapAlloc<Alloc, Key>, which makes the typedef solution even more unavoidable.

@huonw
Copy link
Member

huonw commented Jan 18, 2014

Wouldn't the choice of hash function be better solved using newtypes on the key? More or less the same issue came up for the Haskell hashable package, and it's the solution they seem to have converged on (though it doesn't seem to have been implemented yet).

Maybe; I was just going with our current scheme where the hash uses IterBytes rather than taking the whole value at once, and so it's possible to change the hash by changing the transformation we perform on this byte stream. (This is possibly/probably good to change, but not directly relevant for this bug.)

I note that, using the scheme from your comment, if you want to be able to manually specify only the allocator, even with default type arguments you'll need a type HashMapAlloc<K, V, Alloc> = HashMap<K, V, SipHash, Alloc> typedef, because only arguments at the end are defaulted.

(Just to be clear, I left that one out because, on first thought, it seemed like it would be the rarest; this may or may not be true. I wasn't omitting it to attempt to paper over a deficiency.)

they appear to want type parameters to be in the opposite order

Does HKT require partial application only at the end of a type? This seems rather unlikely to me. Certainly writing impl<K> Functor for HashMap<K> (for the current scheme of struct HashMap<K,V> { ... }) and having the V parameter implicit seems rather peculiar since our "function applications" at the type level are delimited by < ... > rather than just whitespace ala Haskell.

That said, the best other scheme I can think of right now is something like impl<K> Functor for HashMap<K, _>, which probably has other deficiencies.

@glaebhoerl
Copy link
Contributor

having the V parameter implicit seems rather peculiar since our "function applications" at the type level are delimited by < ... > rather than just whitespace ala Haskell.

Indeed, but this is only an issue at the syntax level.

That said, the best other scheme I can think of right now is something like impl<K> Functor for HashMap<K, _>, which probably has other deficiencies.

impl<K> Functor for HashMap<K, ..> might be a good choice, with the meaning "ignore all remaining parameters".

Does HKT require partial application only at the end of a type?

Essentially yes (or rather only at the front, leaving free the parameters at the end, but that's what you meant), otherwise you admit type-level lambdas which don't seem like a good idea.

@nikomatsakis
Copy link
Contributor

cc me

@nikomatsakis
Copy link
Contributor

I am tentatively in favor of this. There is a negative interaction with Higher-Kinded Types: if effectively requires us to make partially applied types explicitly indicated (e.g., HashMap<K, ..>); also, there will be no hiding these extra type parameters from the kind, so HashMap would still be * -> * -> * (Key, Value, Allocator) [I'm side-stepping the question of whether a separate hash trait makes sense].

Particularly as there is no formal write up, I'd like to review the PR before it gets merged to make sure I understand all the consequences.

@nikomatsakis
Copy link
Contributor

Let me amend the last statement: s/Particularly as there is no formal write up/Particularly as I haven't seen a formal write up/ -- not sure if there is something I haven't seen? (I'm quite behind in bugmail etc)

@pnkfelix
Copy link
Member

I'm not yet convinced this buys us all that much over just telling people to use type; in particular, huonw's example of a "particularly bad" instance just didn't look that bad to me. (Especially since it could have been re-written so that HashMap calls HashMapHash, in order to attempt to follow Don't Repeat Yourself:

struct HashMapHashAlloc<K, V, Hash, Alloc> {
    ...
}

type HashMapHash<K, V, Hash> = HashMapHashAlloc<K, V, Hash, LibcAllocator>;
type HashMap<K, V> = HashMapHash<K, V, SipHash>;

@pnkfelix
Copy link
Member

oh, I see, @pcwalton pointed out why this workaround is currently deficient in his opening comment.

Yes, we should talk about whether to enable calling methods through typedefs.

@glaebhoerl
Copy link
Contributor

There is a negative interaction with Higher-Kinded Types: if effectively requires us to make partially applied types explicitly indicated (e.g., HashMap<K, ..>); also, there will be no hiding these extra type parameters from the kind, so HashMap would still be * -> * -> * (Key, Value, Allocator)

Requiring partial application to be explicit might be alright. But this is ignoring the subtler issue: if you want to be able to write a Functor instance for HashMap, then Value has to be at the end. If you want to have a default type argument for Alloc, then Alloc has to be at the end. I don't see a way around this conflict.

@pcwalton
Copy link
Contributor

This is fine per the meeting.

@alexcrichton
Copy link
Member

We did decide though that we would like to put this behind a feature gate (it may be right now, sorry I haven't looked to closely). The feature gate is a little tough in that it needs to gate the definition of types with default type parameters, but it should also gate usage of the default type parameters.

For example:

struct A<Foo = Bar>; // gated
let _: A = ... ; // not gated
let _: A<MyType> = ... ; // gated

@glaebhoerl
Copy link
Contributor

@alexcrichton shouldn't the latter two be the other way around? I would understand "using" a default type argument to mean omitting it and having the compiler supply the default.

@alexcrichton
Copy link
Member

In theory, we would want to change HashMap to HashMap<K, V, A = Heap>, but we don't want to expose this A = Heap detail to anyone not able to change the default right now. The feature gate would allow us to remove the feature entirely and have code continue to compile that wasn't participating in the feature gate.

@glaebhoerl
Copy link
Contributor

I guess either choice is fine as long it's consistent.

@bors bors merged commit 7d96774 into rust-lang:master Jan 30, 2014
@eddyb eddyb deleted the generic-default-type-params branch January 30, 2014 20:06
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

Successfully merging this pull request may close these issues.

10 participants