Placeholder types

If we allow _() form, we might want to clarify that we do not support type inference based on call signatures. i.e.

struct S {
  init(someUniqueLabel: SomeUniqueType)
}
func foo(arg: SomeUniqueType) {
  let val = _(someUniqueLabel: arg) // We don't look for types with matching `init` signature.
}

It's probably obvious, but just in case :slight_smile:

1 Like

Hey @Jumhyn , what's the status of this?
I hit this today:

func getSomething() -> SomeSuperLongAndComplexType?

I needed to make an implicitly unwrapped optional value from this:

let something: SomeSuperLongAndComplexType! = getSomething()

But I wish I could just:

let something: _! = getSomething()
1 Like

Hey @rintaro! A good chunk of the groundwork for this proposal was actually recently merged since the refactor was independently useful.

I hit a bit of a roadblock with handling attributed placeholder types, then got sidetracked and haven’t gotten around to taking another stab. I hope to have a chance to get back to this soon!

1 Like

Thanks for the update! Yeah, I saw the groundwork recently merged. Great work!

I'm looking forward to it!

2 Likes

Update 4/1

Hi everyone, I've finally had a chance to pick this back up and work through a few of the final details. Thank you for all the feedback so far. I'm currently planning for the final proposal to be different than what was discussed here in a couple of minor ways:

  • Attributed placeholder types (e.g., @convention(c) _ = { (x: Int) -> String in "" }) will be deferred from this proposal and left as a future direction. There's a bit more detail in this thread, but the short version is that there's some subtle design issues with this aspect of the feature that make it difficult to integrate with the rest of the model. The workaround in the meantime would be to write the above type as @convention(c) (_) -> _, which will work under this proposal.

  • In general, 'top level' placeholder types will be allowed, so it's legal to write let t: Int.Type = _.self. However, some of the cases brought up in this thread won't be supported (at least at first), e.g., let x: Int = _(), let x: Int = _.init(). In theory, we could look through initializer calls to fill in the base with the knowledge that the base type will be the same as the contextual type, but IMO this is confusing for a couple reasons:

    1. We already have implicit member syntax to support this (i.e., let x: Int = .init()), so we'd end up with two very visually similar forms for doing the same thing.
    2. This would have to be special-cased to constructors, since let x: Int = _.zero expresses something subtly different than let: x: Int = .zero. The latter, by the rules of implicit member syntax, expresses an implicit link between the base and result type, allowing us to propagate type information from "outside" the implicit member expression "inside." The _.zero form expresses no such implicit link—"_" is simply an unknown type, and the member access could be anything.

I'm also interested in the community's thoughts on the effect this proposal may have on existing error messages. Since _ is currently used in patterns (and on the left-hand side of assignments), there's existing diagnostics when it's used outside those contexts. E.g.:

let x: Int = _ // error: '_' can only appear in a pattern or on the left side of an assignment

With the least-careful approach to enabling placeholder types, every currently invalid underscore will be treated as an attempt to write a placeholder type. This means that the above code would produce a diagnostic something like:

let x: Int = _ // error: could not infer type for placeholder

We could also try to implement some heuristics to guess whether the user was trying to write a placeholder type or a wildcard pattern/discard assignment, but we won't be right 100% of the time. I'm curious to hear the community's thoughts about the importance of preserving the existing pattern/assignment-based diagnostics.

(Note that there are some existing (invalid) expressions which, IMO, pretty much have to change their diagnostics in a world with placeholder types, e.g., let t = (_, _).self)

1 Like

I’m missing something maybe, or forgetting something from the earlier discussion, but why is this work expanding beyond type positions? It seems allowing _ to be a placeholder type where arbitrary values are allowed is what’s causing all of this confusion. (Recall that .self is allowed after any value whatsoever.)

My concern, looking at this with new eyes, is that what makes the original proposal particularly intuitive relies on more than just the bare _. Consider:

let x: _ = ...

The _ strongly suggests a placeholder, but it’s the position of the placeholder after the colon that suggests that it’s a placeholder type. Ditto for what happens after a return arrow or between angle brackets. But in the context of the expression _.self, this crucial context is lost, and we’d just be overloading the meaning of _.

8 Likes

I think we could adopt a rule here which didn't allow placeholder types in "value" positions, which would basically restrict them to type annotations and as expressions (other places, too?), but I'm not sure this actually results in a better model. Remember that type values are frequently used to guide inference with functions like:

func decode<T>(_ type: T.Type, from data: Data) throws -> T

So disallowing placeholder types in those positions seems undesirable. Granted, those types of functions are clearly less common than type annotations, but when they're present it's because the library author wants to provide a way for clients to explicitly guide type inference, which is exactly what placeholders are designed to help with.

Also, as it stands today, there's no difference between the grammar of a "type value" and a type, aside from the fact that we have to write .self to get a type value. It would bother me to introduce an inconsistency between those two forms, but maybe that's more explainable than the alternative... :thinking:

This isn’t a situation where the library author merely wants to provide a way for users to guide type inference: this is where the library author wants to require users to be explicit about the type.

Recall that, in Swift, users can always choose to specify explicitly that the genetic return type of a function should be, say, Int by writing f() as Int.

The Swift core team has reiterated that they never want to overload by return type, but even under that design constraint, library authors can offer an optional way to specify a return type using an argument with a default value: that is, _ type: T.Type = T.self. In that case, the type doesn’t have to be spelled out where context makes it unambiguous. And in that case, _.self serves no need.

So what you’d be proposing here is that it would be desirable, in the case where library authors have deliberately chosen to require an explicit type to be written out no matter what, to create a language feature so that users can override that decision. I think not only is that not desirable, it’d be an anti-goal.

3 Likes

To be clear, I think we're in agreement that the bare _.self form is probably of limited utility. But the whole point of placeholder types is to allow the programmer to specify the minimum amount of context necessary in circumstances where Swift (today) requires that an explicit type be written. So it seems perfectly reasonable to me to pass, say, Result<_, _>.self, or ((_) -> _).self to a parameter expecting a type value, if that's all the information needed to disambiguate a call.

In any case, I don't find the argument based on functionality alone the most convincing—my personal motivation for placeholder types would I think be pretty much satisfied if they were just allowed in type annotations and as coercions. However, I find it quite undesirable to introduce an inconsistency in whether placeholders are allowed to be used based on the syntactic position of the type. So given that I don't view placeholders in type values as particularly problematic, I'm inclined to allow it.

2 Likes

Sure, and _ in those cases is not being used in place of a value (or function name, or argument label, etc.), and as a corollary, a type used in that position instead of _ is not required to be suffixed with .self. I'm not referring to those situations when I refer to using a placeholder type in place of a value.

Put another way, there is a difference between saying:

  1. In Swift, _ means "placeholder," and where that place can only hold a type, then _ holds the place of a type.
  2. In Swift, _ means (among other things) "placeholder type."

(2) is what gets you into questions about _.self and _.zero and _() and let x: Int = _, and other absurdities, when plainly (1) permits none of these things.

[Edit] By the same reasoning, it naturally falls out that, while _? and Optional<_> would both be permitted in, say, a generic constraint, foo(Optional<_>.self, ...) is fine but foo(_?.self, ...) is not.

4 Likes

I was writing out a response that called this out, so I'm glad you mentioned it! This is getting into the weeds that I was concerned about—we're essentially splitting the world of type syntax in two in a way that I don't think is particularly explainable. Allowing Array<_> somewhere but disallowing [_] strikes me as worse than just saying "placeholder types are not allowed outside of type annotations or coercions" (which I'm also not particularly fond of).

ETA: The question of "is this syntactic form a type, or not?" is already highly context-dependent in Swift. For instance, [Int: String].self may appear to be a type, but consider:

let Int = ""
let String = 0

[Int: String].self // ["": 0]

That it's possible to come up with arcane type forms using this feature doesn't trouble me too much. It's simply the nature of Swift's lightweight type syntax, and I don't think that placeholder types make the issue considerably worse.

1 Like

As you point out, it is plainly unworkable to make _ mean a placeholder type everywhere because it creates ridiculous situations such as let x: Int = _ being interpreted as a type mismatch. It is simply the case that _ cannot be allowed everywhere Int can: you are already drawing the line somewhere by carving out specific scenarios that you think are clearly unsupportable.

What I'm pointing out is that the most generous way to create a principled rule is to allow _ to mean a placeholder type essentially everywhere that Int can be used without writing Int.self. Or in your terms, where the type-or-not question is not context-dependent.

The principled explanation for bounding the feature in this way is that _ already exists in the language to mean a placeholder in some fashion; there's nothing about the underscore that suggests that the place being held is specifically a type (and, in fact, it does not mean this elsewhere it's permitted). Therefore, since a placeholder by construction cannot give the context necessarily to make a context-dependent type-or-not decision, the underscore can hold the place of a type only where the type-or-not question isn't context-dependent.

This is precisely an example that explains why _ cannot be allowed here. You demonstrate nicely why the _ in [_: _].self doesn't necessarily hold the place of a type: it can just as well hold the place of ["": 0].self. This context-dependency is highly astonishing even to experienced users; it's not a feature, it's a bug.

By contrast, in Dictionary<_, _>.self, there is no such ambiguity. In fact, you can shadow Int and String with values all you want, Dictionary<Int, String>.self will still give you the correct value.

The only factor that makes the expression in the example you give not irredeemably ambiguous is that there is context (that is, there are names and not placeholders) by which the shadowing can be seen to occur. By construction, a placeholder underscore is used precisely so as not to give such context.

2 Likes

I think we should keep separate the discussion of "what's the proper diagnostic for invalid code under this proposal?" and " what syntactic forms, precisely, should this proposal make valid?" Under any of the rules discussed, let x: Int = _ will remain invalid (as it is today), and the diagnostic can change (or not) independently of the specific decisions of this proposal.

As the [Int: String] example shows, these two aren't the same thing—there are context-dependent situations where Int can be used without writing Int.self. IMO, the fact that the language model lacks good terminology to even talk about these distinctions properly is a good reason to avoid opening the can of worms altogether.

Existing uses of _ are disjoint with the valid positions for placeholder types, so in fact by construction placeholder types can give the context necessary to make the type-or-not decision. [_: _].self cannot possibly stand for a single-element dictionary of 'placeholder values' because "_" never produces a value (other than a type). Also, if the type-or-not question is answered by other context, can we use placeholders then? E.g., [_: String].self (where String is in fact resolved to a type), or [String: Array<_>].self.

Disallowing these 'value ambiguous' forms also causes issues for tuples—you can write Array<_> instead of [_], great, but the same issue we've been discussing applies to (_, _).self and there's no such escape hatch.

1 Like

I'm not sure I see how your example shows context dependency where sometimes one must use Int.self and sometimes Int: you can't write [Int.self: String.self] interchangeably where you can write [Int: String] regardless of context.

Mmm, I think the can of worms is opened! If we lack good terminology to explore the design space fully in a shared way, we'll need to establish that terminology rather than just...not doing the work of exploring the design space fully.

To be clear: I don't mean to suggest that the context necessary for the compiler is missing. I'm not approaching this from the compiler implementation perspective, just as when I say that [Int: String].self == ["": 0].self is a bug, I don't mean that there's something that can be fixed by a PR.

I mean all this from the perspective of the human user trying to understand what is going on when they read the code. For the human reader, the underscore does not currently give context necessary to make the "type-or-not" decision because there is nothing inherent to _ that means placeholder type rather than placeholder. (Yes, in the case of your [Int, String].self example, the human would be astonished if the result weren't a type, so we can debate how this is to work under any proposed design; but this unique user-assumes-context-where-compiler-cannot scenario ceases to apply as we get to _(), _.zero, = _, and so on.)

Yes, an underscore can give the context necessary to the user if you stipulate that it does. In other words, you can imbue the underscore with the power to "decide" the type-or-not question by extending the meaning of _ to mean exactly that. Users would have to be taught that where _ appears in the context of a type-or-not question, it provides the context that tells the compiler and user that it's a type.

This is restating again what I've been saying several times now but probably poorly. This proposal has great appeal because the use cases you bring up can be satisfied by building out an existing user intuition where _ intuitively means placeholder. When used in the context of Foobar<_>, or (Foo) -> _ or f() as _, that the place being held is a type is obvious. However, the underscore does not intuitively mean placeholder type: when applied to _() or _.zero or = _ or even _.self, you are not merely building out an existing user intuition, you are stipulating the underscore to mean placeholder type in the user-facing model of the language as a new "overload" of the meaning of the underscore. This would need to bear its own weight, but the additional use cases it enables seem meager at best and actively undesirable at worst.

Just as [_: _].self versus Dictionary<_, _>.self question is worth debating even if we specify that this feature is limited to scenarios where the type-or-not question isn't contextual ("disallowing value-ambiguous forms"), because it's a unique situation where the user would not find it value-ambiguous but the compiler formally does, some of these other questions would be worth debating too.

Having talked this through, I'd revise my position that [_: _].self should "obviously" be disallowed and strongly consider allowing it; and for the same reason, I think it's fine to have a model even if [_: _].self were disallowed to allow [_: String].self. I'd imagine that it is much more likely that the user would want to use the latter form than the former.

Big picture, though: I think it's much more sensible to be working on how to draw the line here than to be trying to draw the line at allowing or disallowing _() and = _.

Can you imagine a scenario in which a user would want to write (_, _).self but could not write _.self, and then by extension, a scenario in which a user would want to write either of these other than where the library author wanted to require an explicit type to be written out? I struggle to think where this escape hatch would become necessary.

1 Like

My point was that the expression [Int: String].self is a situation where "Int can be used without writing Int.self" (whether Int resolves to a type or not), and yet it is not a situation where "where the type-or-not question is not context-dependent," since we must resolve Int and String before determining whether we have a type.

Perhaps I'm still misunderstanding you, but the question of "can we write [Int.self: String.self] interchangeably with [Int: String]" is entirely context dependent, based on whether Int and String are types:

let Int = ""
let String = 0

[Int: String]
[Int.self: String.self]

Ah, I didn't mean to imply that we shouldn't explore this here on the forums—I very much appreciate this discussion, so thanks for continuing to push on it. Rather, I was saying that I don't think we should open this can of worms into the language model. The lines we're drawing here are very thin and very windy, and I think they result in a much more complex/subtle feature than "you can write a placeholder type anywhere you can write a type". I'm not convinced the edge cases you've called out here justify trying to come up with an entirely new syntactic categorization for types rather than relying on the existing type inference rules. But of course we should thoroughly explore the design space. :slight_smile:

Okay, great, we're on the same page here.

Just to clarify: of the forms you listed, only _.self would actually be able to compile under this proposal. The _(), _.zero, and = _ (and _.init()) forms would be naturally disallowed as fallout of existing type inference rules. The only thing to debate in those situations is what the user actually intended to write, so that we can provide useful diagnostics.

Today, of course, all such uses of _ were almost certainly intended to be a wildcard pattern or discard assignment, so the diagnostic is tailored for those situations. But in a world with placeholder types, at least some of those usages seem to clearly be an attempt to use placeholders, and IMO the diagnostic should reflect that. Of course, we could also just give up trying to infer user intention and extend the existing diagnostic to say something like "'_' can only appear in a pattern, on the left side of an assignment, or as part of a placeholder type," but again, I think the question of "what are the best diagnostics in a world with placeholder types" is something that will have to be based on usage, and is a less important part of this proposal.

Glad I've worn you down on [_: _].self. :grinning_face_with_smiling_eyes: I'd still personally consider the second outcome here unacceptable, but happy to move on for now.

As I mentioned before, I think my personal use cases for placeholder types would be almost entirely solved by type annotations and as coercions, so I don't have any great motivating examples here (other than my contention that if we allow let x: (_, _) = ... then we should allow (_, _) as a type anywhere). However, we can trivially contrive an example which I believe satisfies your constraints:

struct Foo<T> {}

extension Foo where T == (Int, Int) {
  static func makeForType(_: T.Type) -> Self { Foo() }
}

extension Foo where T == [Int] {
  static func makeForType(_: T.Type) -> Self { Foo() }
}

let foo1 = Foo.makeForType((_, _).self) // OK
let foo2 = Foo.makeForType([_].self) // OK
let foo3 = Foo.makeForType(_.self) // error: ambiguous use of 'makeForType'

Is this a situation where " the library author wanted to require an explicit type to be written out"? I don't think that's obvious. It could be that the library author wanted to provide an easy way to specify just the generic argument in situations where Foo can be inferred, e.g.:

func takesFoo<T>(_: Foo<T>) {}
takesFoo(.makeForType((_, _).self)) // OK

Perhaps Publisher.setFailureType(to:) is a good real-world example of when (_, _).self might be useful. A downstream operator might be ambiguous without any context (_.self), but fully-specified if "tuple of two elements" is known ((_, _).self). (Edit: aside from the fact that, of course, (_, _) can't conform to Error... :man_facepalming:)


Now, all this said, I am somewhat compelled by the principle of being conservative and letting usage drive future evolution, so from that standpoint the prospect of saying "for now, placeholders are only allowed in type annotations and as coercions" does interest me. But I would only really want to do that if:

  • It didn't introduce unnecessary confusion in the meantime.
  • I didn't think that we'd eventually end up extending it to all type positions anyway.

IMO the confusion of having different type grammars in different positions is worse than the possibility that users might write confusing code using placeholders, and I think we will inevitably have some users who want to use placeholders in type-value expressions and the inability to do so will be an annoying inconsistency in the language until we eventually allow it.

Just to refocus the discussion with the benefit of both of us having thought things through a bit more, and your softened stance on forms like [_: _].self, I'd really love to hear your revised thoughts on the following question:

What are some specific examples of remaining forms that would be valid code under this proposal which you think should be rejected?

1 Like

Sorry, I see the confusion: I have phrased this poorly. By "where Int can be used without writing Int.self," it was specifically meant to exclude Int-resolving-to-a-type-or-value. To rephrase, [Int: String].self is a scenario in which Int-resolving-to-a-type can be spelled without the requirement (or even the option--see below) to write Int.self.

Yes, I think you are still misunderstanding me, but it is my fault in not writing more clearly.

You can try this at home: the last expression above creates a dictionary with one element, with key Int.self and value String.self. It is not equivalent to Dictionary<Swift.Int, Swift.String>.self.

If you have aliased Int and String to values, not only is there not a requirement to use .self to spell Int-resolving-to-a-type in [Int: String].self, you do not even have the option of writing [Int.self: String.self].self to resolve Int to a type.

This is just one example where Swift already does not observe Dictionary<K, V>.self and [K: V].self being interchangeable.

In the case where the author wanted to provide an option rather than a requirement, as I've written before, the author could have written static func makesForType(_: T.Type = T.self) -> Self. The presence of the default allows users to omit the type where context makes it clear.

In this example, under the unlikely circumstance that the only disambiguation needed is to specify "tuple of two elements," such a default would allow users to write:

takesFoo(Foo<(_, _)>.makeForType())
// or, if we really want to allow it...
takesFoo(_<(_, _)>.makeForType())

I would reject _.self, and I would categorically reject (not "at least at first", but forever) let x: Int = _(), let x: Int = _.init().

If we decide to accept [_, _].self, then I would not reject but rather accept let t = (_, _).self. I would also affirmatively accept _<A, B> and even _<_, _>.

1 Like

Alright, I see what you're getting at, thank you for clarifying.

It takes two to miscommunicate. :slight_smile:

Good callout. I think this is, unfortunately, a necessary evil to cope with Swift's lightweight sugared types and lack of a fully-qualified-name syntax. But it's only an issue when [K: V].self resolves to a valid dictionary value, which simply doesn't apply to [_: _].self. The latter is not a valid value, so we'd be disallowing seemingly valid type syntax under this proposal (and indeed, type syntax which is allowed in other places like type annotations) on the basis that it could be confused with a value which is not valid Swift code. That seems far more drastic than the situation we have today with [K, V].self.

I don't really see how that's relevant to the example. Even if the author had provided default T.self values in both makeForType signatures, we'd have the same issue:

let foo1 = Foo.makeForType((_, _).self) // OK
let foo2 = Foo.makeForType([_].self) // OK
let foo3 = Foo.makeForType(_.self) // error: ambiguous use of 'makeForType'
let foo4 = Foo.makeForType() // error: ambiguous use of 'makeForType'

Again, the entire purpose of placeholder types is to allow users to specify the minimum amount of type context necessary to get an expression to type check properly.

The more I think about it the more I like the example of setFailureType(to:)-like functions. These sorts of functions exist solely to get types to match up, so they aren't really about making sure explicit types are visible in source. Indeed, iOS 14 provided overloads of Combine operators such as Publisher.flatMap which automatically apply setFailureType(to:).

Suppose there were an analogue to setFailureType(to:), setOutputType(to:). I think it's an extremely compelling use case for placeholder types to be able to do something like setOutputType(to: Foo<_>.self), and I think it would be very unfortunate if there were certain type forms (e.g., (_, _).self) which simply weren't allowed in that position despite being allowed in type annotations or as coercions, and despite not being ambiguous with any other valid Swift code.

I actually think the setFailureType(to:) family of functions presents a good use case for _.self as well. Consider that placeholder types present a tool to library clients to enable improved type inference even in the face of sub-optimal library design. The definition of setFailureType(to:) doesn't provide a default E.self value, and yet as noted re: Publisher.flatMap above, it's clearly not considered vital by the library authors that the type coercion be spelled out in source.

I think it would be a great use of placeholder types to be able to replace:

self.somePublisher
    .setFailureType(to: WhateverTheRightErrorIs.self)

with

self.somePublisher
    .setFailureType(to: _.self)

Also, FWIW, "at least at first" was mostly meant to be an acknowledgement of the realities of Swift evolution. Even if this proposal were to "categorically reject" those forms, there's nothing to stop a future proposal from making them valid. I don't plan to work on such a proposal (unless someone comes along and convinces me that those forms actually have some compelling use cases, in which case I would just fold them into this proposal). :slightly_smiling_face:

I'm with you all the way up until _<A, B>. My position on these forms described in the original pitch ("skeptical") remains accurate—I don't have super strong reasons to reject them, but right now, AFAIK, the type system has no way to model "an unknown generic base with (potentially) known arguments," and I suspect that supporting this in the type system would be a pretty significant endeavor compared to "vanilla" placeholder types, so really I'd just want to see it independently motivated.

Sorry, I took a break mostly from these forums for a few days on vacation, because...well, vacation.

This is a good point: it's arguing essentially that _ can mean placeholder type instead of just placeholder because it can't mean placeholder value. I would still maintain that ascribing a meaning that's not inherent to the underscore by process of elimination like that isn't very intuitive. But in terms of the pragmatic result, I've come around to your view that for the specific case of [_: _].self that it'd be fine.

I don't understand your point, or perhaps just how it's a counterpoint to my point.

If the author wished to allow the entire type to be omitted where inference is possible, then adding the default value to the function declaration means that, in any instance where makeForType(_.self) would be unambiguous, so too would makeForType(); and similarly, wherever inference is not possible, neither makeForType(_.self) nor makeForType() would be unambiguous.

Thus, the proposed support for _.self is strictly redundant with what's already possible in today's Swift, with the distinction that the one spelling is in the hands of the library author and the other in the hands of the user (on which point, see below).

I don't use Combine (yet), so I can't speak to how it's used in practice.

However, the addition of a default value is an ABI- and API-compatible change, so library authors can decide at any time whether it's optional or not to spell out an argument. In today's Swift, when an API requires a user to pass a type as an argument without providing a default value, it cannot be elided; where type inference is possible, the library author can already use the presence of a default value to give users an option rather than a requirement.

Making it possible to use _.self in the former scenario erases the distinction between those two distinct API design decisions, taking power away from library authors. Whenever possible, Swift language features and their defaults empower library authors to make good libraries (cf. debate about open versus public by default) by giving them tools to shape how users call their APIs; they do not cater to working around suboptimal libraries by users particularly when doing so takes power away from good library authors.


I think this is leaking implementation difficulty considerations into the question of the design we're after.

If your position is that we should support placeholders in all positions where users may want to use it for type inference, then I think is a sine qua non to support _<A, B> way before considering _.self:

It is so natural to come upon scenarios where a generic extension, say, has been implemented for OrderedDictionary where Key == String, such that at the point of use both OrderedDictionary and OrderedDictionary.Key can be inferred but the type of OrderedDictionary.Value cannot. In that case, a user who knows about placeholder types would logically reach for _<_, String> as a spelling (for the same reason that, if the example were a plain dictionary, they'd prefer writing [_: String] over Dictionary<_, String>), only to find (without placeholder support for generic bases) that the first underscore is invalid but the second underscore is fine.

Although I think I've said enough for why _.self specifically is a form I would not support, I would add this though:

.self in the user-facing language is just a "magical" member that works as though defined as a property in a hypothetical extension Any implementing the identity function. If we embrace the logic that permits writing _.self, then I see no logical grounds to prohibit the other forms I earlier suggested rejecting alongside it (_.zero, _.init(), etc.), because those after all are just other members, just like .self.

Buying the argument that we should support placeholders in all positions where users may want to use it for type inference argues for an expansive addition to the language; by contrast, what I argue here is that any argument against _.init(), etc. encompasses _.self also, else we end up introducing an inconsistency between one particular (albeit magical) member and all the others.

1 Like

Not a problem! Hope your forum-free days were relaxing. :slight_smile:

:+1:

I think we may have gotten wires crossed a bit—the original example was in response to this request regarding whether to allow (_, _).self:

I agree with you that this is not a particularly good motivating example for allowing _.self.

I don't know that things are quite as stark as you're presenting here. The most carefully-designed API for some type Foo with a method func takesSomeType<T>(_: T.Type) can be thwarted by any client:

extension Foo {
  func myTakesSomeType<T>(_: T.Type = T.self) { self.takesSomeType(T.self) }
}

I take your point that "just drop in a placeholder" is perhaps easier to reach for as a first reaction than "define a new method in an extension," but it also has the added benefit of being a visual indicator at the call site of "I am explicitly invoking type inference here."

Anyway, my preference overall for allowing top-level placeholder types period is somewhat weak (including in type-only positions like let x: _ = ...), so in the interest of conservative evolution you're successfully pushing me towards a blanket ban on top-level placeholders for the 1.0 version of placeholder types. If the lack of top-level placeholders isn't a pain point in practice, then I think the potential issues you've raised justify leaving it out.

What I do feel more strongly about is allowing let x: _ = ... but disallowing _.self. Whether a particular placeholder form is allowed in one position or another should, IMO, be based only on Swift's normal inference rules and the surrounding context.

/me googles 'sine qua non'

Again, I don't disagree that there are situations where "infer the generic base" is potentially a useful concept (indeed, I unwittingly wrote up a potential use-case for it just above :grinning_face_with_smiling_eyes:). That said, it's definitely not my position that placeholders should be allowed to stand in for any structural part of any type—they stand for types. The bare name of a generic type is not itself a type (any anywhere we allow writing a bare generic name we have explicit rules for inferring the type it actually corresponds to, i.e., inferring the generic arguments).

This is the same reason that the latest revision of this proposal explicitly disallows placeholders in nested positions like Foo._, and the same reason why it doesn't allow inference of "any function type" as _ -> _. Allowing placeholders in these structural, non-type positions is interesting, and as I've noted, definitely useful in some cases.

However, it also complicates the "clean" model of "a placeholder just stands for a type." So while implementation concerns are part of my reasoning for not pursuing more complex constructs for placeholders, I also think it complicates the user-level model for both placeholders and the type system in ways that are not obviously desirable.

I'd ideally like to see a decision to pursue the _<_, String> direction be based on usage of placeholders in the wild, which is why it's left as a future direction. It's a purely additive change, so leaving it out for the moment won't impact our ability to extend placeholders this way in the future.

I could probably be convinced that if we accept _.self then we'll also need to accept _.init(), and in fact, under the current implementation of this proposal (which at the moment permits _.self and friends), forms like _.zero, _.init(), and _() are disallowed only by virtue of the current type inference system, not by any special-casing for placeholder types. As I mentioned up-thread I think we could improve type inference to allow initializer expressions to work just fine.

However, _.zero and similar would never work under the currently-proposed model for placeholder types. Implicit member (chain) expressions are a very special case in the language, where writing let x: Int = .zero is saying "the result type of this expression is the same as the implicit base." OTOH, _.zero expresses no such link, since _ simply means "fill this in using normal inference rules." I suppose we could adopt a special-case rule that _.member means exactly the same thing as .member, but I'm not sure that actually results in a better or more explainable model. If someone down the line wants to make that case, I'll listen, but for the time being I'm not arguing on its behalf. :grinning_face_with_smiling_eyes:

This is something that I didn't understand from your update post: Why is it a problem that _.zero is different from .zero, a difference which requires special rules or otherwise "solving"? Of course it's different; otherwise, why would it be useful to add as a feature?