SE-0328: Structural opaque result types

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