Personally, I'm in agreement with @crontab that the problem of disambiguation can be solved by assuming a type first which is aligned with real-world usage. In the case of an Array literal at least the fix is to just be more verbose in typing the Array's type.
Even though [Int]()
is "normal" Swift, it definitely causes some grossness in the parser and type checker. That doesn't parse as a type at all; it's parsed as an array literal containing a single element Int
, and a function call applied to that. The type checker then looks for expressions like that and converts them to types before solving.
If we allowed users to write Int
instead of Int.self
, then consider this example that becomes ambiguous:
extension Array {
func callAsFunction() { ... }
}
let x = [Int]()
Is this calling an initializer on Array<Int>
, or is it constructing an array with a single element whose value is the metatype Int.self
and calling it as a function application?
Sure, it's unlikely that anyone would write that code, but the type checker has to consider the possibility. Any time you say "the problem of disambiguation can be solved by assuming X", you need to show that adding "X" to some disjunction isn't going to cause a performance problem.
@allevato thanks for illustrating the valid case where this, yes, isn't easy to write away as solvable by just assuming type-first.
That specific example, where extension Array { func callAsFunction() {} }
and [Int]()
might mean array-literal-then-function-call, to me feels like it sits at the absolute fringes of what’s possible in Swift.
Swift IMO as a language has never let super-improbable fringe code hamper everyday usability.
Swift already does far more intricate "overload resolution" and expression rewriting, a few features that come to mind:
- Trailing closure syntax
- A ton of operator disambiguation scenarios (optional chaining vs postfix operators for example).
- Even type inference of something like
Set([1, 2, 3])
where it is inferred correctly asSet<Int>
instead ofSet<Array<Int>>
.
I might be entirely wrong on the complexities of the implementations for the scenarios that I mentioned, so please forgive my ignorance here - would love to know I could be thinking clearer here.
To be concrete, the proposal was deferred because it was uncertain whether it was implementable. To resurrect this proposal, an implementation that is sufficient to demonstrate that it is possible to overcome the implementation-level concerns raised by the core team would need to exist. Discussion in the abstract is unlikely to move the needle here.
I'm not familiar with Swift compiler's inner workings, and having built a compiler once, I'd think it should go like this:
If there's an opening square bracket then:
- if it's in the context of array of type references then parse what follows as a list of type references
- else if the first expression is a type, then expect a closing square bracket and treat it as a reference to
Array<*type*>
- else parse the rest as an array literal
Does it make sense? Or is Swift more complex than that?
To this, I would point to SE-0220: count(where:)
, a seemingly simple addition that was pitched and accepted all the way back in 2018 but didn't truly land until Swift 6.0 specifically because there were type checker performance issues that had to be solved before it was deemed usable. And that's not even involving fringe code.
Parsing happens before semantic analysis. You can't make decisions based on whether an identifier references a type or not at parsing time, because that isn't known yet.
Touché. That particular count(where:)
example makes me sad :(
I think required metatype .self
feels weirder now that identity key paths finally started working last year.
Enum.self[keyPath: \.self] // Compiles, as it should.
Enum[keyPath: \.self] // Expected member name or initializer call after type name
I see the argument about array literal syntax. If that's not involved, I don't think extra .self
helps people.
func takeArguments<each T>(_: repeat each T) { }
enum Enum { case `case` }
takeArguments( // All of these compile.
Enum.self.case.self,
Enum.self.case,
Enum.case.self,
Enum.case,
Enum.self
)
f(Enum) // Expected member name or initializer call after type name
Am I wrong to feel like the main usage of .self
is metatype arguments? I'm open to it being considered disambiguating elsewhere, but I really don't think it does anything to help mentally parse arguments. The compiler won't let you try to use a nested instance of a metatype when defining an instance of an outer metatype.
// This all compiles, as it should.
func f(_: (some Any).Type) { }
f([Int].self)
f([Int.Type].self)
_ = [Int.self].self as [Int.Type]
f([Int.self].self) // Cannot convert value of type '[Int.Type]' to expected argument type 'some Any.Type'
Pull requests welcome ;-)
So today, type expressions are folded in the “pre-check” pass that runs before constraint solving, so in this case we don’t actually consider both possibilities. But the current behavior is also wrong in other ways, for example ().self
is interpreted as the empty tuple value and not the empty tuple type—to get the latter you have to refer to a type alias, for example Void.self
.
What year is it!?
I'm in favour of allowing .self
to be elided. It would make some of Swift Testing's macros simpler (because we wouldn't need to conditionally insert self
, and wouldn't need to try to guess if it's needed.)
I believe E.self
is always allowed for every expression E
(it evaluates to the same value as E
), so you could just always insert it, too.
TIL! What was the background for allowing literally any value to have an implicit .self
property? That feels like a gift that keeps on giving, and ".self" is kind of a hard thing to search for in the archives.
FWIW, if it's possible to always prefer the type interpretation in the case of my tortured callAsFunction
example instead of it being a bottleneck, I'd be all for that because literally nobody should ever write that code.
I believe this was inspired by Objective-C, where the NSObject class responds to self
by returning self
, but I’m not sure because this was before my time on the team. However note that it’s not a real property in Swift, just built-in syntax.
We mostly add .self
indiscriminately for that reason, but there are caveats:
- Rewriting the relevant syntax tree impacts compile time; and
- We sometimes need to deal with constructs like
Foo.bar
which works fine rewritten asf(Foo.self).bar.self
… unlessFoo
is a module name.f(Foo).bar
would still be invalid unless modules suddenly became symbols in their own right, but this proposal would presumably be a step in the right direction.