Enabling implicit member references with nested members

I couldn't find any previous topics on this, so apologies if it has been discussed already.

Currently, the following code is invalid:

class C {
    static let zero = C(0)
    var x: Int

    init(_ x: Int) {
        self.x = x
    }

    var incremented: C {
        return C(x + 1)
    }
}

func f(_ c: C) {
    print(c.x)
}

f(.zero) // Prints '0'
f(.zero.incremented) // Error: Type of expression is ambiguous without more context

Is there anything technical that would keep this from working as expected? It seems like it would still be a simple syntactic transformation, but maybe I'm missing something...

If this were possible, I would expect the following as well:

f(.zero.x) // Error: Cannot convert value of type 'Int' to expected argument type 'C'
// (Currently, 'Type of expression is ambiguous without more context')
class C {
    static let b = B()

    var x = 0
}

class B {
    var c = C()
}

func f(_ c: C) {
    print(c.x)
}

f(.b.c) // Prints '0'
// (Currently, 'Error: Type of expression is ambiguous without more context')

This most commonly comes up for me when working with types like UIColor that provide transformation methods that return modified versions of the color, e.g.:

view.backgroundColor = .red.withAlphaComponent(0.5) // Not allowed!
7 Likes

I donβ€˜t have any links for you but this was definitely discussed a lot of times.

The only thread I could find that seemed relevant for a search on "'implicit member'" was this one which didn't really go anywhere other than raising the point that it should work through more than one level. Is there some other terminology under which this might have been discussed?

1 Like

I think this is related: My personal beef with leading-dot syntax

"leading dot" does turn up some additional examples, thanks for that! That thread in particular, though, doesn't really seem to discuss the "multiple levels" at all. There is this thread where @jrose offers a summary of the current rules around implicit member expressions:

However, I'm more interested in knowing whether there is a reason that that rule can't be expanded to cover multiple levels of reference.

I'm afraid this is one of those things that seems straightforward but blows up in the general case.

What would be the scope in which to search for the chain of member references? The contextual type must be the type of the full expression. Is the root of the search also the contextual type?

If so, then the search is limited to chains starting and ending in that type. Unfortunately, the search space between those points is unbounded and grows exponentially. (In your second example, imagine that instead of mutual recursion from C β†’ B β†’ C, we had C β†’ B1 β†’ B2 β†’ … Bn β†’ C, and each Bi had a dozen properties.)

If the root of the search is not the contextual type, then the search space is substantially all of the global name space β€” static methods/properties, initializers, and all cases.

These problems can be avoided by limiting the length of the reference chains. The current implementation sets that limit at 1.

What differentiates this from the case where I've written the contextual type explicitly, e.g., print(C.b.c)?

In other words, why does the compiler need to "search" at all, if we've written out the explicit chain of references, as opposed to just following the normal lookup rules as though the contextual type had been written explicitly?

2 Likes

I think this is manageable, as long as the chain is required to begin and end with the same contextual type. There's a functional dependency implied by member lookup; we don't look up members until we have a type resolved to look into, so the search space shouldn't be expanded any more than it would normally be if we start from the contextual type of the entire expression.

6 Likes

Good to know!

Does that imply that the proposed error in the original post is problematic? Or would we still be able to resolve the type of the member access as proposed and offer a more specific error than "type is ambiguous"?

@xedin might be able to comment on the diagnostic issue.

1 Like

Thanks for clarifying, @Joe_Groff.

We have recently ported a diagnostic for this to the new diagnostic framework so if you try with master you'd get a following diagnostic:

error: cannot infer contextual base in reference to member 'zero'
f(.zero.incremented)
  ~^~~~

The problem is that result type of reference to incremented is disconnected from its base type because it's considered a regular member reference (references like .zero would connect base type and result type via equality constraint so it's possible to infer the base from result). That's why although result type of the chain could be inferred there is no way to propagate information in the opposite direction.

2 Likes

Would it be possible to look down through to the end of a chain of member accesses and impose the equality constraint between just the base type and the final result type, or is that infeasible?

I think it's possible and a right thing to do for chaining with contextual members. @Joe_Groff Do you think that this would require an evolution proposal or should we consider it a bug fix?

4 Likes

It seems appropriate for a (hopefully not very controversial) pitch and proposal. It'd be good to get attention to any possible technical or ergonomic issues with the feature.

1 Like

Sounds good, thanks @Joe_Groff. I can put together a pitch soon and see if the implementation seems like something I can take on.

3 Likes

Is this a problem requiring the prefix of the class be done explicitly? I’m all for explicit. Better readability, maintenance.... but how about a Modula 2 solution? The WITH bracketing?
Example:

class C {
    static let zero = C(0)
    var x: Int

    init(_ x: Int) {
        self.x = x
    }

    var incremented: C {
        return C(x + 1)
    }
}

func f(_ c: C) {
    print(c.x)
}

with C
f(.zero) // Prints '0'
f(.zero.incremented) // Error: Type of expression is ambiguous without more context
end with

β€”β€”β€”
It’s a time saver without resorting to hard to maintain implicit usage, can be nested, and is explicit for readability. It also saves the compiler hunting for a match.
Please excuse my inexperience with formatting in posts.