Okay. Are you thinking that you'll also want to provide opaque
return types, or is that a deferred goal?
I still want to provide opaque
return types, because I think that's the common case where you don't want to write out the type in all its detail and don't need conditional conformance.
Doug
Okay. So you'll still need this special syntax, where
clauses and all, even if theoretically that might be obviated by a future generalized-existentials proposal?
I've been assuming that generalized existentials will use the same where
clauses, so that the only different between returning an opaque result type and returning a generalized existential will be the opaque
keyword, e.g.,
func foo() -> opaque Collection where _.Element == String {
return ["a", "b", "c"]
}
func foo() -> Collection where _.Element == String {
if Bool.random() {
return ["a", "b", "c"]
} else {
return Set(["a", "b", "c"])
}
}
Doug
And that's disambiguated from the existing use of where
in that position by the presence of a clause rooted on _
?
For generalized existentials, we could take the same approach I outlined with opaque result types, where you effectively say that the constraints in the where
clause apply to the returned generalized existential whenever it makes sense. Things get a little interesting with a result type like Collection where _.Element == C.Element, _.Element: Equatable
. "Obviously" this means that C.Element: Equatable
is a function-level requirement, and that _.Element: Equatable
is part of the generalized existential.
Doug
@John_McCall @Douglas_Gregor It indeed does look entangled if it gets verbose with the underscore syntax. I am confident that a better approach, as already mentioned above, is to allow in-place naming for both opaque and opened existentials. This way we can list conformances without _:
; it becomes more readable and we can allow mixing the constraints as usual. Also, we would have open doors for a clean future syntax for expressing conditional conformances in signatures. In my opinion, it's far more natural and native-looking. The only difference is that the given name is declated after the arrow, to emphasize this isn't a generic parameter. Comparison:
func foo<T>(_ arg: T) -> opaque Collection & SomeOtherProtocol where _.Element == String,
T: MutableCollection, T.Iterator == _.Iterator,
_: RandomAccessCollection -> T.Element: RandomAccessCollection { ... }
func foo<T>(_ arg: T) -> opaque U: Collection where U.Element == String,
T: MutableCollection, T.Iterator == U.Iterator,
U: RandomAccessCollection when T.Element: RandomAccessCollection { ... }
The approach you outlined with opaque result types is maximal munch, though, not "whenever it makes sense".
This feels really weird. I understand how it works when typechecking the function body, but doesn't this basically mean that some subset of the requirements have to be recognized statically as not applying to callers? The polarity is flipped, in a sense.
I already expressed my opinion above, but I would like to take the chance and outline that Iād really not want to see this _.Assoc
syntax on existentials as well. It is too cryptic and is not better syntax-wise as other options that were posted in this thread. If you really want to allow the ambigious where
clause everywhere instead of forcing it to a typealias then I suggest to rethink the _.Assoc
syntax and consider something like a generalized $0
syntax instead. If each returned value can be seen as an implicit tuple of one or more values (one element tuple is unlabled), then the $0/1/..
syntax starts making a lot of sense. In general this syntax can be handy in typealiases as well.
My question is how .swiftinterface
describes globalValue1
and globalValue2
has the same type?
In this case, .swiftinterface
for module A
would be:
public protocol P {
}
public var globalValue1: opaque P { get set }
public var globalValue2: opaque P { get set }
We can't expose generateP()
here, right?
I've been thinking about this too but haven't come up with a satisfactory answer yet. I'm sure we can make something up, though.
@abiName("generateP.result (except mangled)")
opaque typealias ResultOf$generateP: P
public var globalValue1: ResultOf$generateP
public var globalValue2: ResultOf$generateP
Another option is to just say that you can't do this with an anonymous type; you'd have to use an explicit opaque typealias instead.
Yeah, either of those work. I was imagining we would introduce a spelling to name the opaque result type based on the declaration, e.g.,
pubic var globalValue1: opaque_result_of ModuleName.generateP()
... but that's basically a specialty syntax for the opaque typealias, so your idea is better.
Doug
Ah, I talked to @Slava_Pestov about this and we realized that uniquely naming a declaration is actually very hard (even the complete type context, full name, and argument types is not necessarily enough in the presence of particularly complex overloads), so we shouldn't rely on being able to do it in an automated fashion.
I'm not thrilled with any of the syntaxes we've seen thus far:
-
_
has the wrong meaning -
opaque
orreturn
don't work for generalized existentials -
requiring the entity to be named feels verbose
-
$0
is not technically ambiguous with closure parameters but makes the boundary feel fuzzy. -
.Element: Equatable
lets us think of the missing type as the context type, like the leading-dot syntax we have elsewhere, but it doesn't let us write things like_.SubSequence == _
. We'd also need to addSelf
as a member to make that work, e.g.,.SubSequence == .Self
.Doug
I like giving the opaque type a name and using when
instead of where
for conditional conformances.
I am just not sure whether you intended to use the name just for opaque types and not for existentials. In my understanding both would use a name and opaque types would use the opaque keyword:
// Existential
func foo<T>(_ arg: T) -> U: Collection where U.Element == String,
T: MutableCollection, T.Iterator == U.Iterator,
U: RandomAccessCollection when T.Element: RandomAccessCollection { ... }
// Opaque type
func foo<T>(_ arg: T) -> opaque U: Collection where U.Element == String,
T: MutableCollection, T.Iterator == U.Iterator,
U: RandomAccessCollection when T.Element: RandomAccessCollection { ... }
I'd be open to use as placeholder $
instead of a name (and _
), though. This would still allow the rather nice syntax for conditional conformances which would not be possible using .Element
.
// Existential
func foo<T>(_ arg: T) -> Collection where $.Element == String,
T: MutableCollection, T.Iterator == $.Iterator,
$: RandomAccessCollection when T.Element: RandomAccessCollection { ... }
// Opaque type
func foo<T>(_ arg: T) -> opaque Collection where $.Element == String,
T: MutableCollection, T.Iterator == $.Iterator,
$: RandomAccessCollection when T.Element: RandomAccessCollection { ... }
Yes, that's the idea. The placeholder isn't too bad, but that isn't the 'swifty' way, right? You can feel this cryptic deviation from the native syntax without necessarily any gain in brevity (clarity over brevity, as @jrose would say). For someone new to the language, especially, I find -> Collection where $.Element == Element
to be less intuitive and readable than -> T: Collection where T.Element = Element
. If we say
-> $: Collection where ...
, it already looks much better, but then why use a symbol?
IMHO, the issue I have with opaque types is that the concrete type is inferred from a call site. As a result, would this not be a heterogenous array?
func foo() -> opaque Collection & Equatable { return "foo" }
func bar() -> opaque Collection & Equatable { return "bar" }
let foobar = [foo(), bar()]
Would I be unable to do something like this?
if foo() != bar() { ... }
If these aren't possible, then uses like this seem to undermine the usefulness of opaque types in a (hopeful) future with generalized existentials.
Opaque types do not eliminate all use cases for existentials. In your examples, foo()
and bar()
do indeed have different types from the type system's perspective, and you'd need an existential type or type-erasing container to be able to work with both as one type.
Or use an opaque type alias to declare that both foo()
and bar()
return the same opaque type as discussed in some previous posts:
typealias Foo = opaque Collection & Equatable as SomeConcreteType
func foo() -> opaque Foo { ... }
func bar() -> opaque Foo { ... }
let foobar = [foo(), bar()] // homogeneous array containing elements of SomeConcreteType
if foo() != bar() { ... } // valid code
How about a generic-parameter-list-like syntax after the arrow?
func foo() -> <T : Collection> T where T.Element == String