SE-0328: Structural opaque result types

With SE-0328, you can create some P in parameter position easily with typealias, even if we ban bare some P in that position.

typealias Closure<T> = (T) -> ()
let closure: Closure<some Numeric> = { (x: Int) in print(x) }

typealias using type parameter more than once creates same-type constraint:

typealias Tuple<T> = (T, T)

// t0.0 and t0.1 have the same type, but t1.0 and t1.1 don't necessarily have the same type
let t0: Tuple<some Numeric> = (0, 0)   
let t1: (some Numeric, some Numeric) = (0, 0)

However, when it is only once that the type parameter is used, the behavior should be consistent with its expanded form, because there are no additional constraint. Therefore, I think it's natural to expect (some P) ->() and Closure<some P> to behave in the same way.

There are only two possible plans: whether to accept bare some P in parameter position or not. While acceptance of that is supported by the argument that the behavior of (some P) -> () should be equal to that of Closure<some P>, banning is supported only from the view of 'future direction'.
I'm not strongly opposed to ruling out bare some P in parameter position from SE-0328, because they can be added in the future. But typealias can create some P in parameter position easily, and (some P) -> () as Closure<some P> is pretty natural interpretation. Banning them seems too artificial limitation to me.

1 Like

I have followed the discussion around some P in parameter position for a while now and wanted to add my two cents to it.

For me some P indicates: This is some specific type which is chosen by the callee and I (the caller) cannot see, which type got chosen exactly.
The sugar that many people want now (namely that func foo(_ bar: some P) is equal to func foo<T: P>(_ bar: T)) doesn't fit with that description of some P at all.

I would much rather want that some P in parameter position means what it already means today (at least in diagnostics and quick help).
This would just work with closures, since we could just write:

let closure: (some BinaryInteger) -> () = { (value: Int) in
    // ...
}

to get the desired behaviour.
With normal functions however, that wouldn't work like it works with closures. Here we would need a way to tell the compiler what the actual underlying type is. I could imagine some syntax like

func foo(_ bar: some BinaryInteger) where bar: Int {}

but this feels a bit strange, because normally everything in the where clause is part of a function's signature, but here it would be not.
So maybe we would have to find a better syntax for that, but I still think that this functionality of some P in parameter position would be more consistent than that it sugars a (very simple to write) generic function.

2 Likes

Good callout. I would hope that if we did defer some P in parameter position as part of this proposal, we would be able to ban it a bit more comprehensively than a purely syntactic rule like "some P may not appear inside the parameter list of a written function type." Ideally, the rule would be something like, "a user-written some P may not be resolved in a way such that the underlying type is contained within the parameter list of a function type." I am not quite knowledgeable enough about opaque type resolution to know how feasible that sort of rule would be from an implementation standpoint, though.

1 Like

With the implementation of this proposal, it works just like any other type parameter - a some type is opened and replaced with a type variable in the constraint system, and bindings are inferred in the same way as usual. We could easily track that the type variable originated from an opaque type with a locator (the implementation might already do that, I remember discussing that approach with @bdriscoll a while back)

EDIT: The implementation does indeed already do that:

/// This is referring to a type produced by opening an opaque type archetype
/// type at the base of the locator.
CUSTOM_LOCATOR_PATH_ELT(OpenedOpaqueArchetype)
1 Like

I don’t completely understand the goal of this. What does it mean for a callee to choose the type of its parameters? At the call site, the compiler needs to prepare the arguments and jump to foo()’s implementation. In your scheme, what precisely are the concrete types of the values on the stack at the point that execution jumps to foo?

Counter-question: What is the concrete type of bar in the following code snippet?

func foo() -> some BinaryInteger { 5 }

let bar = foo()

An opaque BinaryInteger archetype. Swift code cannot directly construct instances of such a type. Therefore such a type cannot be the type of a function parameter.

How does that work then?

You can still construct instances of archetypes as long as you have an initializer requirement. Opaque types are trickier to construct because you can't actually spell the archetype in a type expression context, but the ExpressibleBy*Literal protocols still allow you to construct instances of opaque types from literals.

And you can also use initializer requirements and static member expressions, e.g. if you have a function foo with type (some BinaryInteger) -> () you can call it with foo(.zero), or foo(.init(truncatingIfNeeded: 5)).

1 Like

But you aren’t constructing values whose concrete type is opaque:

func foo() -> some BinaryInteger { 5 }

let bar = foo() // type of bar is 'some BinaryInteger'

print(type(of: bar)) // prints "Int"

The value stored in the memory location named by bar is an Int. The type of the bar variable within the type system at compile time is some BinaryInteger.

You have expressed a desire that foo(_ arg: some P) “means what it already means today”. I am trying to understand what you think it “already means”, because I cannot come up with a definition of opaque return types as they exist today in which the callee exerts influence over the types of its arguments.

I think this framing of the problem with "overloading" the meaning of some P in parameter position makes a lot of sense; thank you for taking the time to explain in depth.

I've also been thinking more about the case where an inferred opaque type in parameter position is useful, and I'm slowly coming to the conclusion that the some spelling isn't really the best syntax anyway a few reasons:

  • It doesn't properly express its relationship to the opaque type that it was inferred from, which is going to inform how you use that type/what type of value your'e allowed to pass in
  • The spelling only possible if that opaque type is constrained to a protocol

In the example from upthread:

func foo() -> some BinaryInteger { 5 }

let bar = foo() // type of bar is 'some BinaryInteger'

let baz = bar.isMultiple(of:) // type of baz is '(some BinaryInteger) -> Bool

Spelling the type of baz as (some BinaryInteger) -> Bool is...actually kind of misleading. It's not a random some BinaryInteger. The type of this thing would better be expressed with some static variant of type(of: bar). Then, it would be clear that you don't have to guess at the type of value to pass in - you can pass in anything that has the same type as bar, such as the result of calling foo() again.

3 Likes

Just a question... In what situation isn't an opaque type constrained to a protocol?

When it's inferred from an unconstrained associated type of an opaque type:

protocol P {
  associatedtype A
  func f(a: A) -> Void
}

struct S: P {
  func f(a: Int) { }
}

func someP() -> some P { S() }

let p = someP() // this type is `some P`
let fn = p.f    // this type is... `(type(of: p).A) -> Void`?

I suppose you could express the input type of p.f with some Any but that doesn't seem particularly helpful either :slightly_smiling_face:

3 Likes

I agree and the diagnostics, in the easiest cases, are complete in that regard. An opaque type isn't diagnosed just as some P, but as some P (result of 'foo()') or some P (type of 'bar') depending on the situation, which is great.


Okay, okay... the fix-it is horrible, but the error is complete and on point!

However, there can be cases in which the referenced variable or function is too convoluted to be spelled in quick help pop-ups or even in errors. In those cases, the spelling some P seems to be the best one IMHO. One of such examples may be this one

func foo() -> some Collection { "s" }
foo().index(after: 5)
// error@-1: Cannot convert value of type 'Int' to expected argument type '(some Collection).Index'

The error shows up because the returned Collection may not have Int indices. The complete spelling would have been (some Collection (result of 'foo()')).Index, which is acceptable in this particular instance, but it wouldn't have been anymore if e.g. foo() had showed up in a method chain. (As a side note, in your last example, fn is ((some P).A) -> Void, so no need to fallback to (some Any) -> Void :slight_smile:)

That to say that some P in parameter position (relatively to structural opaque result types) is the most appropriate spelling which can be applied in the general case.

The some keyword, while never advertised as doing this, allows for patterns that had been previously reserved for class hierarchies, and protocol hierarchies without associated types. One of the ramifications of that is in my previous post. Another other is that you can now utilize base members in "overrides". E.g.

protocol Super {
  associatedtype Associatedtype = Any
}

extension Super {
  var property: Int { 1 }
}

protocol Sub: Super { }

extension Sub {
  var `super`: some Super { self }
  
  // Returns 2.
  var property: Int { `super`.property + 1 }
}

If Super had no associated type, (self as Super).property + 1 would compile, but it's not as good of a solution. So self as some Super should compile regardless of whether Super has an associated type.


Interestingly and sadly, it's possible to use type members as well, but only if you have an instance to work with. :face_with_monocle: I don't think this proposal is going to help with that, but I hope I'm wrong!

extension Super {
  static var property: Int { 1 }
}

extension Sub {
  var property: Int {
    type(of: `super`).property
  }
}

@hborla quick question:

Why is this an invalid position?

Wouldn't the usage inside generic context allow the same usage inside a generic type alias which in other words could be just substituted to just some P?

typealias G<T> = T
typealias SomeP = G<some P>

I understand that an opaque type needs to be resolved from a return type, but so can the aliased opaque type only be resolved at the point of its use in for an opaque type valid position.

It's a totally new rule. I think this should be discussed as generalized some syntax, not as structural opaque result types.

In addition, you can just do this:

// instead of
typealias SomeP = SomeComplexStructure<some P>

// use this ... Some<some P> can be used for SomeComplexStructure<some P>
typealias Some<T> = SomeComplexStructure<T>

Review Conclusion

The proposal has been accepted.

5 Likes