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).
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.
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.
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.
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) -> _?
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
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)
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 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...
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...
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.
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>
?
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.
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.
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.
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.
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).
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.