I think your analogy makes total sense if we view the relationship between generic parameters at the head and tail as totally unbound. I just think about it a little bit differently such that the "default to the contextual parameters" idea makes more sense to me. Note that this idea (in my mind) only applies to defaulting generic parameters at the head of the chain, so with your example:
extension X {
func clone<U>() -> X<U> { X<U>() }
}
let _: X<Int> = .new().clone()
This should continue to be an error in a world with multi-member chains. The new inference rules would get us as far as:
(((X<Int>).new() as X<_t>).clone() as X<Int>)
But there are no additional rules for resolving generic parameters along the chain.
Yeah, it's definitely getting into very subjective territory about what feels "too magic" when it comes to inferring these parameters. I'm going to try to rephrase my thinking as to why, if we're going to support these different generic parameters at all, we should be as aggressive as possible about inferring the generic parameters at the base from the tail.
In a simple world with no overloading, single member chains are basically a simple lexical substitution. In a declaration like:
let _: Int = .zero
or
let _ = 3 + .zero
you simply have to find the declaration that gives the contextual type and "copy-paste" it to the base of the implicit member reference. If we move to multi-member chains, there's really no difference—the result at the end of the chain is entirely determined by the specified members as part of the chain, and we can again perform a simple lexical substitution: find the declaration(s) that give us the contextual type, and copy-paste that type to the head of the chain.
Introducing overloads complicates this model a little, but not too much—the members along the chain can actually help us resolve ambiguous overloads, but this again feels like a straightforward extension from single member chains, where this compiles today:
func f(x: Int) -> Int { return 0 }
func f(x: String) -> String { return "" }
let _ = f(x: .zero)
However, the simple case (where we're referencing only un-overloaded declarations and the type can be entirely determined from context outside the chain) continues to work exactly as expected.
Introducing generic parameter inference to the story again complicates the model a little, allowing for even more "advanced" usage of implicit member expressions. Now, this works:
struct S<T> {}
extension S where T == Int {
static var sInt: S<Int> { S<Int> }
}
func f<T>(s: S<T>) {}
f(s: .sInt) // ok!
Again, this extra inference power does not interfere with the simple case, for single or multi-member chains.
My ideal model for this "different generic parameters allowed" rule is that it's just another step along this path. That is, it functions as an "advanced inference" feature for users who are using heavily generic code and many chained methods/properties. It shouldn't, however, interfere with the "simple case" by breaking the "just lexical substitution" model.
Completely breaking the link between the generic parameters of the head and tail, though, does break that model:
let _: Result<Int, SomeError> = .success(0).mapError { SomeError($0.code + 1) }
A user with the "simple model" in their mind would be surprised at this being an error, but without some sort of link between the generic parameters at the head and tail, there's no way to type-check this. As you've mentioned, we need to support it at least for the single-member-chain case, and that results in multi-member chains no longer being an "intuitive" extension of single-member chains.
Now that you've helped me with generating some potentially problematic examples for this "different generic parameters" rule, I'll probably take it back to the pitch thread and see what other members of the community think about them.
This is a really good illustration of where the defaulting rule starts to break down, since it requires us to make a value judgement about the two different kinds of defaulting. It's easy enough to construct an example for single-member chains as well:
struct Adder<T: AdditiveArithmetic> {
init(x: T, y: T) {}
static func add<U: AdditiveArithmetic>(x: T, y: T) -> Adder<U> {
print(x + y)
return Adder<U>(x: .zero, y: .zero)
}
}
let _: Adder<Int8> = .add(x: 127, y: 127) // overflow, or no? (today, the answer is overflow)
In order to maintain full source compatibility, the rule has to be that generic parameter defaulting always beats out literal defaulting even though there are two literal defaulting failures in one case and only one generic param defaulting failure in the other.
I'm still curious how you were imagining modeling the "equal up to generic arguments" constraint. As-is, just dropping in an equality constraint is too aggressive. Depending on the order in which constraints are resolved, the equivalence classes for the head and tail might get merged before there are any generic arguments to pull out of the tail to create the defaultable constraint. My hack right now involves getting really specific about how this particular equality constraint gets resolved, which does not seem like the right way to do things. E.g., in matchTypes (
):
// Instead of merging the two type variables at the head and tail of
// an implicit member chain, let the tail get resolved to a concrete
// type first. This way, if it's a generic type, we can defer
// binding the type params to one another, allowing for different
// params at the head and tail of the chain.
SmallVector<LocatorPathElt, 4> path;
auto anchor = locator.getLocatorParts(path);
if (auto *UME = getAsExpr<UnresolvedMemberExpr>(anchor))
if (path.size() == 1)
if (path.back().getKind() == ConstraintLocator::MemberRefBase)
return formUnsolvedResult();
What I want is sort of like a OneWayEqual constraint, except that I only want to wait until the "outer layer" is resolved (rather than the entire type), and I want the handling upon resolution to be different than just inserting a fixed binding.
The defaulting rule helps us bind free type variables, but if the lookup-based generic parameter inference can find two different results that are equally good, then defaulting doesn't do much for us. I'm guessing this is the same reason that we have a score kind for "non default literal" which is what's used to properly type-check a program like this:
let x = f(x: 1)
func f(x: Int) -> Int { return 0 }
func f(x: UInt) -> UInt { return 0 }
If $T1 is the type of the literal, then as soon as we attempt $T1 := UInt, we increase the score and resolve the ambiguity between the overloaded fs. For member chains, the scoring would similarly say "if there's ambiguity, the programmer probably meant for the generic parameters to match the contextual type as closely as possible".
Thanks for the paper! I'll give that a read. And again, thanks for your thoughtful engagement on this topic. You've really helped me organize my thoughts in a way that I think will make taking these questions back to the pitch thread much more productive.
My only remaining question is about how best to model the "equal up to generic parameters" concept (and whether a new constraint kind is appropriate), so if you have any thoughts on that I'd love to hear!