Opaque result types


(Douglas Gregor) #101

Hmm. This section was meant to describe the differences. Can you say a little more about how/why that section misses the mark? I don't have a good sense of how to improve it.

Doug


(John McCall) #102

Okay. Are you thinking that you'll also want to provide opaque return types, or is that a deferred goal?


(Douglas Gregor) #103

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


(John McCall) #104

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?


(Douglas Gregor) #105

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


(John McCall) #106

And that's disambiguated from the existing use of where in that position by the presence of a clause rooted on _?


(Douglas Gregor) #107

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


(Anthony Latsis) #108

@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 { ... }



(John McCall) #109

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.


(Adrian Zubarev) #110

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.


#111

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?


(Jordan Rose) #112

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.


(Douglas Gregor) #113

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


(Jordan Rose) #114

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.


(Douglas Gregor) #115

I'm not thrilled with any of the syntaxes we've seen thus far:

  • _ has the wrong meaning

  • opaque or return 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 add Self as a member to make that work, e.g., .SubSequence == .Self.

    Doug


(Thorsten Seitz) #116

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 { ... }

(Anthony Latsis) #117

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? :slight_smile:


(David Waite) #118

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.


(Joe Groff) #119

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.


(Thorsten Seitz) #120

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