Placeholder types

Yes, thanks, fixed!

Since we're digressing, I'll be shrinking this

What's the new APIs we can express? While I do agree it's useful, I still see this:

struct Wrapper<T> where T == Dictionary {
  func foo(_: T) -> T { ... }
}

as the same API as this:

struct Wrapper<Key, Value> {
  typealias T = [Key: Value]

  func foo(_: T) -> T { ... }
}

I can still do about the same amount of things (with a lot more boilerplate).

1 Like

Another metric that makes sense to consider is the expected rate of usage of each proposed featureā€”itā€™s less costly to choose a heavier spelling for a feature which appears only in, e.g., generic where clauses than it is for a feature which may appear in any expression or local type annotation.

ETA: And, FWIW, I think that the ā€œplaceholder typesā€ usage is a strong contender along both of these axes.

2 Likes

A more practical example of what I'm talking about appears here in an associated type constraint; I had to add KeyPathProtocol and complicate the constraints on Projections because I couldn't express the associated type constraint Focus: KeyPath<T, _>.

And just to put a fine point on it because IMO there's a really a substantial difference between sugar and things that change API expressivity: two APIs are not the same just because they allow the same functionality to be expressed, just like C and Swift aren't the same language just because you can write binary search in either one. Two APIs are different when they cause users to write different code to when using the functionality.

1 Like
Yet another digression, the next one will be a new thread, I promise

Agreed but, the caller are still writing the same thing as you'd expect them to just use Dictionary and not DictionaryProtocol. That's the point I'm trying to convey. What's changed will be on the implementation side, which would be substantial, but invisible to the API users (unless you need ABI compatibility). I guess that means it's not just sugar to the API author side. :thinking:

2 Likes

The first example contains an error:

-let stringTransform = foo as (String) -> Int
+let stringTransform = foo as (String) -> Double

Or you could change the first example to:

let losslessStringConverter = Double.init as (String) -> Double?

losslessStringConverter("42") //-> 42.0
losslessStringConverter("##") //-> nil
let losslessStringConverter = Double.init as (String) -> _?
1 Like

Thanks, Ben! Will update the proposal accordingly. I really like your alternative exampleā€”it feels much less contrived than the one I have there currently.

I like the feature, and I am coming around to the _ spelling.

I had always thought of using _ as a shorthand for "any type can go here", but maybe * can be used for that?

var a: Array<*> = [1,2,3,4]

let b:Array<let T> = a /// I also like this syntax for unpacking a type at runtime

The only point of confusion is with pattern matching, where _ allows matching with any value (not just a specific unnamed value).

This is a typo, right? It looks like a fusion between Swift and Kotlin :slight_smile:

Putting aside the _ placeholder, this is something already mentioned in the "medium priority list" of the Generics Manifesto. You can find it under the Parameterized extensions subsection:

Your syntax would be probably be spelled in the following way:

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

with Key being available exclusively in that declaration scope.

@Alejandro is tackling parameterized extensions (here are some insights with his current implementation), so if this proposal gets accepted and a proposal about parameterized extensions comes out, your suggestion would be more appropriate to be mentioned in the future directions section of that proposal, I think.


I think we should stick with _. In the future values could be used as generic parameters (Vector<Int, 3>, Vector<Int, 2>, etc.). It would be weird to force users to distinguish between types and values with * and _ respectively:

let e1: Vector<Int, 3> = (1, 0, 0)
let e2: Vector<*, 3>   = (0, 1, 0)
let e3: Vector<*, _>   = (0, 0, 1)
2 Likes

The Rorschach languageā„¢ strikes again!

Thanks for pointing that out, that is indeed the same kind of constraint. Considering that it's framed as being just for extensions I'm not sure I we can say it's mentioned there, but it's definitely related.

IMO that syntax runs against the spirit of changes we made long ago, moving parts of a generic declaration that don't describe structure to the back. For example, IIRC, it used to be possible to write

struct X<A, B: P where B.C == A, D>

and where clauses on generic functions used to appear before the return type.

In the example you gave, Key is not a part of the structure of the type being declared, and putting it at the head of the declaration gives it too much weight.

Good point that these things should be tied together. IMO it would be a mistake to tackle these things as though they were separate features unless there was some real implementation difficulty, though. We have enough contextual inconsistencies already regarding what can be expressed in the generics system.

1 Like

+1 overall.

A few corner cases:

  • Placeholder member type:

    struct S {
      struct Inner {}
    
      func overloaded() -> Inner {  }
      func overloaded() -> Int {  }
    }
    
    func test(val: S) {
      let result: S._ = val.overloaded() // Calls 'func overloaded() -> Inner'
    }
    

    I don't think this should be supported because of the same reason as " Placeholders for generic bases"

  • Initializing placeholder types

    func foo() -> Result { _() }
    

    Assuming [_](repeating: "foo", count: 12) is supported (as per "Generic parameter inference"), initializing _ alone probably should be supported too. But we have alternative spelling .init(), so...

1 Like

Thanks Rintaro!

Agreed. Will update the proposal to this effect.

This is a good point, but I canā€™t come up a great rule to prevent this duplicate spelling that doesnā€™t feel at least a bit surprisingā€”I donā€™t think we should allow a different set of types in initializer position than in, say, annotation position.

IMO the only reasonable option would be to disallow a pure-placeholder type everywhere. On the one hand, some usages are clearly pointless (e.g. as _), but OTOH I can see a reasonable case for, e.g., let x: _ = ... as a more-explicit indication of ā€œthis type is inferredā€ (and the pure-placeholder type in initializer position is clearly useful, we just already have another spelling for that).

Ultimately Iā€™m inclined to allow the pure-placeholder type, because the inconsistency of disallowing it feels worse to me than the duplicate spelling, but Iā€™d love othersā€™ thoughts on the matter!

Another question along these lines that youā€™ve triggered for me is whether we should allow:

_.staticMember

As an alternate spelling for

.staticMember

?

Hm, maybe we should just disallow a pure-placeholder type... :sweat_smile:

I think this is a reasonable conclusion. By construction, we have type inference today in those circumstances with an existing syntax, so allowing this is purely duplicative without clear wins. I think it would not only be reasonable but even an improvement to adopt the general principle that _ permits the user to opt into inference where otherwise it would not be possible to do so.

1 Like

One follow-up thought; canā€™t recall if itā€™s been addressed above:

Given a generic type Foo<T>, I can write Foo(...) to initialize a value, with type inference for the generic argument. Within the implementation of Foo, however, I can write Foo as a shorthand for Self. So, identical spelling but different inference rules.

With the use of an underscore, should we say that Foo<_> means the same thing as Foo in both contexts? That would be a very justifiable answer, and probably the simplest. After all, _ is meant to tell the compiler to infer, not how to infer. On the other hand, that would mean that it wouldnā€™t provide type implementers a way to have Foo<_> behave the same way in all contexts.

Further, now consider a type Bar<T, U>. Inside the implementation of type Bar itself, Bar is synonymous with Self, but if Bar<_, _> is also, how about Bar<_, Int>?

1 Like

Yeah, this is addressed in this portion of the proposal:

Basically, the equivalence between S and S<T1, ..., Tn> inside the body of S doesn't follow any of the normal inference rules. In particular, S may be written in locations which normally do not participate in type inference at all (e.g., function signatures). IMO, trying to describe this behavior in terms of placeholder types (or to implement placeholder types in a way that captures this exception) is more trouble than its worth, since it isn't strictly related to the usual type inference provided by the constraint system. (In fact, placeholder types should allow you to recover the "normal" meaning of a bare generic type within that type's body by writing S<_, ..., _> when you really do want the generic arguments to be inferred).

If you think it would be helpful to be more verbose here I'm happy to update the proposal to that effect.

1 Like

Perhaps a worked example would be helpful in the text. I think there will be pitfalls whatever the design ultimately is, so it's helpful to talk through them rather concretely.

Consider the following example (works in today's Swift):

struct Bar<T, U>
where T: ExpressibleByIntegerLiteral, U: ExpressibleByIntegerLiteral {
    var t: T
    var u: U
    func frobnicate() -> Bar {
        return Bar(t: 42, u: 42)
    }
}

let bar = Bar<UInt16, UInt32>(t: 21, u: 21)
type(of: bar.frobnicate())
// Bar<UInt16, UInt32>

Given your proposal, let's consider some variations on frobnicate().

extension Bar {
    func frobnicate2() -> Bar<_, _> {
        return Bar(t: 42, u: 42)
    }
    func frobnicate3() -> Bar {
        return Bar<_, _>(t: 42, u: 42)
    }
    func frobnicate4() -> Bar<_, _> {
        return Bar<_, _>(t: 42, u: 42)
    }
    func frobnicate5() -> Bar<_, U> {
        return Bar(t: 42, u: 42)
    }
    func frobnicate6() -> Bar {
        return Bar<_, U>(t: 42, u: 42)
    }
    func frobnicate7() -> Bar<_, _> {
        return Bar<_, U>(t: 42, u: 42)
    }
    func frobnicate8() -> Bar<_, U> {
        return Bar<_, _>(t: 42, u: 42)
    }
}

If I understand correctly, you propose that the behaviors be as follows--

type(of: bar.frobnicate2())
// Bar<UInt16, UInt32>
type(of: bar.frobnicate3())
// N/A
// Error in implementation: cannot convert return expression of type 'Bar<Int, Int>' to return type 'Bar<T, U>'
type(of: bar.frobnicate4())
// Bar<Int, Int>
type(of: bar.frobnicate5())
// ??? I'm actually not sure.
type(of: bar.frobnicate6())
// ??? I'm actually not sure.
type(of: bar.frobnicate7())
// ??? I'm actually not sure.
type(of: bar.frobnicate8())
// ??? I'm actually not sure.
1 Like

All variations of frobnicate except the original, frobnicate3, and frobnicate6 would be compilation errors under this proposal:

frobnicate3 is acceptable because the return statement is aware of the return type of frobnicate3, and so the two placeholders will be correctly inferred as T, and U. OTOH, it would be an error to write the body as:

let bar = Bar<_, _>(t: 42, u: 42)
return bar

because the generic arguments would be inferred as Int, and Int, since the inference for the type of bar no longer has return-type context.

1 Like

That's much simpler than I had thought. I'll have to think about this, but on first blush it makes sense. I think an example like this in the text would be very helpful to readers.

1 Like

While discussing about an ExpressibleByTupleLiteral protocol last year, it has been suggested to directly initialize with tuples instead of .init in environments in which the compiler can correctly infer the right initializer:

_() fits well from both an explicit and ergonomic standpoint in that regard, in my opinion.


Some off-topic considerations

It's true that Key isn't needed before the structure name, but if you look in the Generics Manifesto section cited before, it has also been proposed an alternative and more compact syntax which involves the direct placement of the auxiliary parameter types, i.e. Key, in the structure name:

If we allow parameterized extensions without a where clause, we cannot annotate auxiliary parameter types there. They should be put after extension and, generalizing, after struct (and let and var for variables? Take a look at the Generic constants subsection).

1 Like

Yes, but I'm not sure that we should allow parameterized extensions without a where clause. That's not necessarily a constraint on expressivity: we could allow an otherwise-empty where clause, used only to introduce parameters. That would at least put the syntactic emphasis in the right place at the cost of writing one additional keyword. I think conditional constraints are rare enough that it's probably a good trade-off.

P.S. I took your hint and posted in the other thread, in case you'd prefer to discuss it over there.