Placeholder types

This proposal looks nice to me. It makes perfect sense and dovetails with existing functionality well.

12 Likes

Loved it!

I think _ works well, since it has basically the same meaning in pattern matching. Except that we're matching types here, not instances. IF only there was a capitalised version of _...

Aha, so this comment reveals a possible reason to limit just to generic arguments. Conceivably, not all generic parameters will be types in the future (for example, if we ever create fixed-size arrays, one expects that the size would be a generic parameter).

Therefore, _ could be (a) restricted to generic arguments and agnostic to whether those arguments are types (obviously, in its implementation today, there would be nothing to change, but I mean conceptually); (b) restricted exclusively to types anywhere that they can be inferred, but not available for, say, inferring the size of a fixed-size array; (c) unrestricted in both respects--but I wonder if that becomes a little bit wibbly wobbly.

1 Like

In the (semi?) distant future where the generics system supports parameterization by scalars, I would expect (b) or (c). If "_" is permitted to stand for a type in a generic signature, it would be surprising to me that it could not stand for a type elsewhere.

Whether we allow it to stand in for scalar generic arguments should, IMO, depend on whether the inference of those arguments is permitted. That is, if I could write:

struct FixedArray<Element, let size: Int> { ... }
let arr = FixedArray(1, 2, 3, 4) // Inferred as `FixedArray<Int, 4>`

Then I would expect to also be able to write:

let arr = FixedArray<_, _>(1, 2, 3, 4)

Or, if we wanted some sort of signifier that the second underscore does not correspond to a type:

let arr = FixedArray<_, let _>(1, 2, 3, 4)

In this hypothetical, perhaps "placeholder types" becomes a bit of a misnomer ("type signature placeholders"?), but the possibility of scalar generic arguments doesn't dramatically change my expectations about where _ should be able to be used.

6 Likes

I think this is a valid concern, but it doesn't have to do with the feature itself, only the use thereof.

By that I mean:

  1. Introducing this feature does not cause existing code to stop compiling (because you can't use as (String) -> _ now)
  2. The only way to get to the problem you're describing is by:
    1. Introducing this feature
    2. Using it (for example, with as (String) -> _)
    3. Creating another overload.
  3. It seems to me that you can avoid problems with this new feature by simply not using it (for example, specifying as (String) -> Int).
  4. It also seems to me like this feature can't break any existing code because there's no existing code like it.

Overall, +1 from me, this seems very helpful.

1 Like

I was wondering last week why swift didn't have it haha. When you're not sure if this feature is presented in that way, you're naturally trying to replace a type with _
So will perfectly fit

1 Like

If we're serious about maintaining a difference between type placeholder/wildcard and value placeholder/wildcard, we can also use ~ vs _.

I'm not in favour for _ and suggest something like $Foo or ~Foo.

The reason is, that in a future version it should be possible to write

let a: Array<~Foo> where ~Foo: Equatable = ...

Of course this can be done with _ too, but not when we have two or more placeholder types.

Another idea is that the placeholder can be reused like this:

let a: ~Bar where  ~Bar: Comparable  = ...
let b: ~Bar = ...
return a < b

Even something like a predefined return type could be expressed:

let r: ~Result = ...
return r

And last but not least tuples can be constrained to the same type:

let t: (~T, ~T) = ...

Placeholder/wildcard patterns are used when you don't need a reference to the matched entity:

_ = functionThatReturns()
// we don't need the return value

let (_, second) = (1, 2)
// we don't need the first tuple element

let a: Dictionary<String, _> = ["one": 1, "two": 2]
// we don't need the type 'Dictionary.Value'

What you are suggesting involves declarations (typealias declarations in this case) similar to how switch statements do. We explicitly require var/let in case statements, so for a similar feature typealias may be more appropriate than a ~ or $ prefix type operator.
Your suggestion may be orthogonal to this proposal.

6 Likes

(overline, U+203E) and _ (low line, U+005F) have a good upper-lower relationship. The downside however is that is a bit harder to type.

1 Like

If I'm understanding the pitch correctly, this is just a way to abbreviate types when part of the type pattern can be deduced, essentially just a bit of syntax sugar. As a general matter, I think we focus too much on this sort of feature at the expense of doing the hard work of solving real expressivity and performance problems, of which we still have many.

In fact, when I saw the Generic Constraints section I got excited because I thought it was going to be more than just sugar, allowing us to express the constraint that some type is an instance of particular generic type. For example, this struct would wrap any collection of Dictionarys:

struct Wrapper<T>(x: T) where T == Dictionary<_, _> { … }

The inability to express that today causes cumbersome and expensive workarounds like:

/// Really just a Dictionary; it exists because we can't express the
/// constraint we want.
protocol DictionaryProtocol: Collection {
    associatedtype Key
    associatedtype Value

    /// `self`, with static knowledge that it is actually a Dictionary
    var asDictionary: Dictionary<Key, Value> { get set }

    //
    // Write out all dictionary method/property signatures as requirements here.
    // Yes, that's A LOT!
    //
}

extension Dictionary: DictionaryProtocol {
  var asDictionary: Self { get { self } modify { yield &self } }
}

// Now I can finally define `Wrapper`.
struct Wrapper<T: DictionaryProtocol> { ... }

However, in its full expression, that feature also needs to be able to express more general constraints on T without introducing new generic parameters to Wrapper, which means we need to be able to introduce new named type variables elsewhere. For example, this syntax might express that “T is a Dictionary with Comparable keys:”

struct Wrapper<T> 
  where<Key: Comparable> T == Dictionary<Key, _>
{ ... }

which of course is just shorthand for:

struct Wrapper<T> 
  where<Key: Comparable, Value> T == Dictionary<Key, Value> 
{ ... }

Because I see no where clauses in the thread, I assume this pitch isn't trying to address any of these limitations. Is that correct?

All that said, I think the risks of adopting the proposed syntax is low, and it fits in well with an abbreviated syntax for solving the more substantial problem I care about, so having registered my dissatisfaction with the priorities reflected, I'm luke+1 :space_invader: on this pitch. As a practical matter it seems obvious to me that we can't allow the underscore in return type position of public function signatures, and I would prefer not to allow it return type position at all except in closure signatures.

4 Likes

We probably have a different notion of sugar, because I think the feature you mentioned, um, lets call it Constraint on Incomplete Generic (CIG), are not providing any capabilities beyond reducing boilerplate, to which I say, is a sugar. That is, I see that:

struct Wrapper<T> where T == Dictionary<_, _> { ... }

is essentially a sugar for

struct Wrapper<TKey, TValue> where Key: Hashable {
  typealias T = Dictionary<TKey, TValue>
}

since both do provide the same functionality. Don't get me wrong, T is a much leaner baggage to carry around compared to (TKey, TValue), so it's a very sweet sugar, but sugar nonetheless. We could probably argue whether Wrapper<T> should be treated as having a single generic parameter T or two generic parameters TKey, TValue at an ABI level, but I don't think that'd change anything about its capabilities.

This pitch could even be a pretty good foundation for CIG as we now have a notion of type placeholder (whether or not we use _ for CIG).

2 Likes

Yeah, that's a consequence of the fact that Swift users far outnumber the number of compiler engineers able to implement those deeper features. If it was a choice between one or the other, I'm sure everybody would agree that solving expressivity/performance issues are more important. But there are still smaller features that can make a positive difference.

As regards the "T is a Dictionary with Comparable Keys" question (which isn't related to what's being pitched here IMO), I think a better way to spell that would be to use the : operator:

struct Wrapper<T> where T: Dictionary, T.Key: Comparable {
  var dict: T
}

I don't like the idea of introducing extra generic parameters in where clauses; it makes it harder to see what the real type parameters are -- and they matter. For example, the above code could also be written like this (parameterised on the key type, instead of the whole dictionary):

struct Wrapper<T> where T: Hashable & Comparable {
  var dict: Dictionary<T, ?> // <- This would be an existential
}

Each of these can have very different characteristics, including execution time and code-size, making it important to consider which "level of meaning" your generic types/algorithms are parameterised at.

2 Likes

Is this really the same feature @dabrahams is talking about? Judging from the work around, it doesn't seem to introduce new kind of existential.

1 Like

Yes. The existential in the example is only required because Dictionary has 2 generic parameters but the wrapper is only parameterised WRT 1 of them. If you substituted Array in the example, you wouldn’t need an existential.

Again, not really related to this pitch. I just wanted to touch on the syntax aspect. If you want to declare constraints between the dictionary’s parameters, you can refer to them as T.Key and T.Value, rather than binding them to new generic sub-parameters.

1 Like

Yes, you've understood correctly. This pitch is just about reducing annotation noise in type-inferable contexts.

I think these portions of the proposal address your concerns, but let me know if I've misunderstood:

Given that this is a relatively lightweight sugar proposal, I'm most interested in these aspects of the above comments:

I do question whether we'd want to overload the meaning of "_" in type signatures down the road. While most people seem on board with the proposed meaning of "_" in this thread, there have been several other suggestions for additional meanings (e.g., existential erasure of generic parameters, or as in your post, Dave, some sort of implicit generic parameter).

Given that these placeholder types would not be permitted to appear in generic where clauses, it doesn't seem like there's a strict technical limitation to adopting "_" for all these features. However given the already extensive overloading of "_" (argument labels, discard assignment, wildcard pattern), I feel that at some point we'll hit a proposed usage that makes us stop and say, "wait, this would definitely be too many meanings for '_'."

Now, I'm still proposing this spelling for placeholder types since it seems most users intuitively understand it in this context, and no one has raised vehement objections—but anyone who feels that this spelling steps on the toes of their favorite hypothetical feature should feel encouraged to speak up.

If some other feature related to types were spelled _, then I would surmise that many users would be confused why the feature you're proposing isn't also spelled _, particularly if it exists but is spelt differently (kind of like how users unwittingly write existential types when they're probably meaning to reach for generics or opaque types).

It seems to me that if multiple features spelled _ can't co-exist in this area, then none of them can be spelled _ without causing such user confusion. At the very least, _ should be devoted to the least likely to be unwittingly used of these features; it shouldn't be an unwitting door into some sophisticated concept in type theory or difficult-to-understand performance cliffs.

5 Likes

The main problem with just using : is that it can't distinguish between T: SomeClass<…> (“T is derived from some concrete SomeClass”) and T == SomeClass<…> (“T is some concrete SomeClass”). But that said, I appreciate that you don't like seeing another parameter list, and that's a separable question. One could use your syntax with == instead. I fear there may be important constraints we can't express without introducing type variables, but I might be mistaken… I'll have to think on that.

I'm afraid that's really not equivalent. In my formulation Wrapper<Dictionary<Int,Int>> and Wrapper<Dictionary<Int,String>> are distinct types.

Is this a typo?

Terms of Service

Privacy Policy

Cookie Policy