[Pitch] Opaque parameter types

@hborla glad you jumped in and clarified that. :slightly_smiling_face: I can wait until the proper evolution proposal then. Thank you again for the clarification. :+1:

1 Like

I don't usually pollute the thread with +1, but I'm a sucker for aesthetics and easy to read/understand code. Also, any feature that slims down the already verbose generic declarations is great news. So, +1. Thanks!

2 Likes

Bikeshedding attempt to incorporate where clause:

func foo(a: some RawRepresentable with Equatable RawValue) {}

perhaps way too english :rofl:

For reference, you could have looked at the RUN line at swift/opaque_parameters.swift at 49bc1edbd7b97a2d373b8f9a3ccf85eff10c1950 · DougGregor/swift · GitHub, which shows both hidden feature flags. Testing that separately-developed-and-pitched features compose together properly into an elegant result is a good thing.

Doug

8 Likes

If v1.Type1 were independently a good idea with its own supporting use cases, this would fall out of that choice. However, I don't think that extending opaque parameter types in this direction is appropriate: if you need to refer to the same generic type parameter more than once in the declaration, give it a name.

Doug

8 Likes

I was curious about such use case:

func batchAccept(_: (some P)…)

Although some P… is technically not available, (some P)… should be accepted as <T: P>T…. Thus I think the expression of an opaque type cannot be used in a variadic parameter is not precise and can be somehow revised.

1 Like

Since this is a new feature, I think it would be prudent to disallow (some P)… in favor of generic parameter packs Variadic Generics and if Parameter Packs are not implemented then the old syntax can be expanded.

The same ambiguity described in the proposal occurs in any structural position, e.g.,

func f(_: [some Hashable: some Equatable]...)

could be interpreted as either

func f<_T1: Hashable, _T2: Equatable>(_: [_T1: _T2]...)

or

func f<_T1: Hashable..., _T2: Equatable...>(_: [_T1: _T2]...)

I think it's prudent to ban uses of opaque types in variadic parameters until we know what variadic generics will look like.

Doug

11 Likes

Personally, I interpret some P as “a single unspecified implementation of P”.

If that interpretation is valid, this:

is unambiguously equivalent to this:


This doesn’t actually make any sense:

[_T1: _T2]... is fine, that’s a variable number of Dictionary instances. But what would the Key and Value be in one of those instances? A variable number of types conforming to Hashable and Equatable respectively? Impossible: Key and Value each need to be exactly one type. In fact, they’re explicitly stated to be a single implementation using some!

The shorthand I’d expect to describe what I think you mean would be:

func f(_: any [some Hashable: some Equatable]...)

That reads as “a function that takes a variable number of instances of any type of Dictionary[1], where the Key is Hashable and the Value is Equatable”.


I’d like to see opaque types become much more pervasive throughout Swift. They allow a programmer to use a specific type while conveying what about that type they actually care about[2]. I think that’s profoundly useful for improving code readability, particularly when refactoring[3].


  1. A generic structure, not a protocol, though the same principles apply. ↩︎

  2. In other words, they can act as natural type aliases. ↩︎

  3. “Does this algorithm actually need that local variable to be an Array, or just some Sequence?. If the latter, I could make this lazy to avoid unnecessary allocation.”

    let doubled: some Sequence = [1, 2, 3].map { $0 * 2}
    

    That sort of thing could also be determined by examining how it is used or reading comments, of course, but this kind of deliberate generalization can make it much easier to interpret. It would compile the same, obviously, though this could speed up type inference in the same way SE-0315 does. ↩︎

There is a variable number of Dictionary instances, each of which can have different Key and Value types. For example, you can pass [Int: String] and [String: Double]. The Key types are stored in the type parameter pack _T1, and the Value types are stored in the type parameter pack _T2.

Both interpretations of opaque types in a variadic parameter type make sense. The variadic-generics version is more general, because it accepts a superset of inputs.

Doug

5 Likes

Which means that _T1 isn’t a Key, and _T2 isn’t a Value. Right?

The main thing throwing me off here is that the type is specified with a simple dictionary literal: [_T1: _T2]. When I look at a dictionary literal, I think “This is one type[1]

typealias [some Hashable: some Equatable] = Dictionary<some Hashable, some Equatable>

To accept any dictionary that meets those conditions, wouldn’t you say just that?

any Dictionary<some Hashable, some Equatable>

I’m not sure I follow. Dictionary isn’t a concrete type: the associated types are part of the concrete type (and not the instance). At the language level, Dictionary<Int: String> and Dictionary<String: Double> are completely different types, connected only by sharing, for lack of a more accurate term, “conformance” to Dictionary.

In other words, there’s a variable number of instances, each of which can have different Dictionary types. That’d only be one variadic generic.


  1. Or “This resolves to one value,” depending on the context. Since this is in a type signature, it’s obviously a type. ↩︎

Both _T1 and _T2 are parameter packs, i.e., each represents a sequence of zero or more types.

I feel like your concern is about variadic generics, not opaque parameters. Let's dig into this function more:

func f<T1: Hashable..., T2: Equatable...>(_ dicts: [T1: T2]...)

There are no opaque parameters here. The variadic generics proposal under discussion is clear about what this means. Let's form a call:

``swift
let intsToStrings: [Int: String] = [:]
let stringsToDoubles: [String: Double] = [:]
let intsToDoubles: [Int: Double] = [:]
f(intsToStrings, stringsToDoubles, intsToDoubles) // okay: T1 = { Int, String, Int }, T2 = {String, Double, Double}


As you can see, the arguments to `f` are all dictionaries, but instantiated with different key and value types. Because `T1` and `T2` are parameter packs (declared with the `...`), they represent multiple generic arguments, as seen in the lists above.

If you drop the `...` from either generic parameter, you get a different API. For example, we could require that all of the value types be the same:

```swift
func g<T1: Hashable..., T2: Equatable>(_ dicts: [T1: T2]...)

// ...

g(stringsToDoubles, intsToDoubles) // okay: T1 = { String, Int }, T2 = Double
g(intsToStrings, stringsToDoubles, intsToDoubles) // error: T1 = { Int, String, Int }, but T2 can't be both String and Double

Dictionary is a generic type, but it's not a protocol and there is no conformance to it. Dictionary<T, U> is a particular specialization of the generic type: for different T and U arguments it will create a distinct type, but they are all specializations of the same generic type and have the same structure. The structural similarity is hugely important to Swift's type-checking model for generics.

Doug

1 Like

+1 in general

What is the story around diagnostics and error messages? Since this is partially about the learning curve, I think that how we would refer to each type in func horizontal(_ v1: some View, _ v2: some View, _ b1: some Button, _ b2: some Button ) -> some View I can't think of anything better than _SomeView1 and _SomeButton2 but think I want to explicitly come out against _T1 and the like if we can do better

3 Likes

func foo<T> (dict: [T: String]) {} is possible today without explicit T: Hashable notation. Is func foo(dict: [some Any: String]) {} also possible?

The compiler will currently spell out the type as the opaque type, e.g., it will say some View. It'd also like it to highlight (e.g., in the editor) the source range where that some View occurs, to help with disambiguation. There's some affordance in the compiler already to give longer descriptions if there are two opaque types that look similar but aren't, e.g., the two some Views in the horizontal example could be distinguished by, e.g., some View (for parameter 'b1').

Ah, yes, this is requirement inference. It is supported, so the latter infers the Hashable requirement on the opaque type.

Doug

2 Likes

The first is that opaque parameter types can only be used in parameters of a function, initializer, or subscript declaration, and not in (e.g.) a typealias or any value of function type

I have one more question; is the next code possible? It's a parameter of function and also value of function type. Which restriction has priority?

func foo(closure: (some Any) -> ())
// equal to
func foo<T>(closure: (T) -> ())

It's a small thing, but in SE-0328, () -> (some P) is allowed. Therefore, I guess "any value of function argument type" or similar explanation is more appropriate in the quote.

1 Like

Given the restrictions on using opaque types in argument position in function values and type aliases, presumably this code does not compile?

struct S {
  static func staticFunc(_: some Any) { }
  func instanceFunc(_: some Any) { }
}

type(of: S.staticFunc)
type(of: S.instanceFunc)
type(of: S().instanceFunc)

Where precisely does the failure come from? Does the language still have no way of representing these functions’ types, or does the failure come from some hard codes check that prevents constructing the argument to type(of:)?

Second question: is func f(some any Any) legal?

Yes, that's allowed. I'll see if I can word things more clearly in the proposal.

Doug

The failure is that there is insufficient context to infer an argument for the implicit generic parameter. The proposal notes the issue by example (but it isn't explained -- I can fix that):

func f(_ p: some P) { }
let fAmbiguous = f                // error: cannot infer argument for `some P` parameter

It's the same failure you would get if you'd declared f with a generic parameter, because the Swift type system doesn't have first-class generic functions, i.e., you cannot have a value of type <T>(T) -> Void: context must provide a specific binding for T.

No, the operand to some should be a protocol or protocol composition, not an existential type.

Doug

Might this feature be added in the future? If so, is this proposal forward-compatible with it? (I suspect it is, but it may not be able to express all types that a syntax with explicit type parameters could express).

I can’t recall whether func f<T: P>(any T) is legal. If it is, why shouldn’t func f(some any P) be legal? “Because the first version is clearer” is a valid answer, but a bit of an odd one.

What about func f(some (any P).Type)?