Using the bare name of a generic type within itself

Currently, inside of a generic type, when the name of that type is used without specifying the generic parameters, it is inferred to have the same generic parameters as the type it is inside. For example, inside an instance method, the use of the bare type name is the same as the type of self, and inside a type method it is the same as the type on which it is called.

Most of the time I appreciate this convenience. However, there are some situations where the type name would ordinarily have its generic parameters inferred from context—such as arguments passed to an initializer—but when that exact code is placed within an extension of the same type, suddenly the generic parameters are not inferred from context but are instead rigidly fixed to be those of the type itself.

As a toy example to demonstrate the issue, consider this function which wraps an Int in an Optional, then prints its debugDescription:

func foo() {
  let x = 1
  let y = Optional(x)
  print(y.debugDescription)
}

Pretty simple and straightforward. However, suppose this were instead a method on Optional. If we write identical code:

extension Optional {
  func foo() {
    let x = 1
    let y = Optional(x)   // error: 'Int' is not convertible to 'Wrapped'
    print(y.debugDescription)
  }
}

…we get a compiler error!

Everywhere else in the language we can write Optional(x) and it works as expected, with the generic parameter being inferred from the value passed into the initializer, except here. Here it’s a compiler error, because Optional is treated as meaning the same type as self.

In order to make the code compile, we have to spell out the generic parameter:

let y = Optional<Int>(x)

Now, we’re looking at just about the simplest possible case. If instead we were dealing with the result of chained Sequence operations, we might have to write out some whole mess as the generic parameter:

Slice<ReversedCollection<LazyFilterSequence<LazyMapSequence<Range<Int>, Int>>>>

This is, frankly, absurd.

I consider it actively harmful that the syntax of “Type name followed by initializer arguments from which the generic parameters are inferred” breaks when being used within that type itself. Calling an initializer on a type should work the same no matter where the code is located.

6 Likes

I always felt like this was an odd feature. I mean, I get why exists, code like this would be a pain:

struct Dictionary<Key, Value> {
    func foo() -> Dictionary<K, Value> { // repetitive, imagine hundreds of parameters and returns repeating this type name along with the generic type variable names

    }
}

I think a more natural solution would be to remove this feature when the rules around Self's availability are loosened up:

struct Dictionary<Key, Value> {
    func foo() -> Self { // Better!

    }
}
2 Likes

In my heart, I take it a step farther and wish that we had Self available and that we couldn't use the bare name. That often seems like the better way to resolve the inconsistency. I am often bitten by the inference, especially on return types.

1 Like

I agree, the natural spelling for “The type of self” is clearly Self.

Additionally, this would also simplify the process of conforming to a protocol, because then you’d be able to simple copy-and-paste type signatures that use Self from the protocol to the conforming type. By contrast, today those signatures must be manually altered in each conforming type, to replace Self with the name of the type.

2 Likes

I had a quick glance in this thread, so bare with me if I missed something, but SE-0068 I believe it was, and it‘s now implemented on master, so you can use Self like this on structs already.

2 Likes

Oh nice, I thought SE–0068 that had been lost to the mists of time. Glad to hear it’s back on track!

However, the problem described in the original post here remains:

Within a generic type, uses of the bare name of that type still mean the same thing as “Self” will with SE–0068, and they cannot have their generic parameters inferred from context like they can elsewhere outside that type.

1 Like

Thank your summing it up for me. This is, from my perspective at least, aligned with the generic type that omits its generic type parameter list in extensions. For example we can just write extension Result { ... } and do not need to write the generic types with all their constraints over and over again. I can kind of understand why inside the generic type the bare type always is viewed as Self instead of Self with different generic types inferred from some context.

Would be interesting to see if the core team thinks that it would be reasonable to change that restriction a little.

In the comments of SR-4155 it looks like the Swift team would be open to removing this "feature", but it would require an evolution proposal.

5 Likes

Thats a good sign especially now that we have Self we can even cover some scenarios where this feature was used and simply migrate to Self if required.

1 Like

I am not proposing any change to how extensions are introduced, only to how the unadorned name of the generic type is treated within an extension (or the main body) of that type itself.

Now, it would obviously be source-breaking to entirely eliminate the current behavior, though migration to use Self could be done automatically. However, I think we can avoid most of that by simply making the current behavior a fallback to be used in case the generic parameters cannot otherwise be inferred.

That is, we can say “Within a generic type, when the bare name of that type is used, the generic parameters are inferred from context, and the lowest-priority possibility is to use the same generic parameters as the type we are currently inside.”

That way, the current behavior still works (you can use the bare name to mean Self), but also the desired behavior works too (you can write let y = Optional(x) for any x).

There is one narrow scenario where the behavior would change with this, and that is when the generic parameter of Self is a supertype of the inferred type:

protocol Foo {}
extension Int: Foo {}

extension Optional where Wrapped == Foo {
  func foo() {
    let x = 1
    let y = Optional(x)
  }
}

Today, y is a Foo?, whereas with this change it would become an Int?.

Luckily, with SE–0068, the fix to keep the existing behavior is simple: let y = Self(x). This is the same automatable migration that would happen if the existing behavior were entirely eliminated, but it will only be necessary in a tiny fraction of situations if we keep that behavior as a fallback.

1 Like

There's an actual reason why it exists, one contrived case where naming the parameters correctly may not be possible, but I can't produce it right now and it might have to do with shadowing anyway, which means it's "your" own fault. I'd personally be glad if someone attempted to get rid of this, but unfortunately that is definitely a source-breaking change, and so we'd probably have to do it next time we want to introduce a new -swift-version. And even then we'd want to be careful about it.

3 Likes

Good find, thanks!

…but with SE–0068, that case can now be spelled Self, right?

• • •

Also, in addition to the original problem of having to write out the generic parameters longhand, I think the most recent example I posted shows that the current behavior is not only troublesome when writing code, but also when reading code, as it harms clarity at the point of use:

extension Optional where Wrapped == Any {
  func foo() {
    let x = 1
    let y = Optional(x)
  }
}

Any reasonable Swift developer, upon seeing this code, should expect that y is an Int?, just like it would be anywhere else. But—surprise!—it’s actually an Any?.

4 Likes

Aha, I remember the terrible example:

struct Outer<Element> {
  struct Inner<Element> {
    func produceOuter() -> Outer {
      // …
    }
  }
  // …
}

This looks especially contrived, but if Outer and Inner are both Collections they don't have a choice about the name "Element". It's still something you can work around with a typealias, though, even if Self isn't sufficient.

3 Likes

Ah, very nice!

Still, with the idea of “Use the current behavior as a fallback in case the generic parameters cannot be inferred”, that example would remain workable as written, with no changes.

Edit:

Also, there are still other things that are equally difficult to write already, which the shorthand does not help with and therefore typealiases are necessary:

struct Outer<A, B> {
  typealias OuterA = A
  typealias OuterB = B
  
  struct Inner<A, B> {
    func invertedOuter() -> Outer<OuterB, OuterA> {
      // …
    }
  }
}

It might be interesting to consider a way to reference generic parameter types à laOuter.A”, but that’s beyond the scope of what I’m looking to accomplish here.

Keep in mind that Self is not the same as the explicit, statically-known type when you’re dealing with classes.

4 Likes

Here comes qualified lookup for rescue, no? :thinking:

struct Outer<Element> {
  struct Inner<Element> {
    func testElementTypes() -> Bool {
      return Element is Outer.Element // qualified lookup (not possible yet)
    } 
  }
}
2 Likes

Excellent point.

I think this pretty well cements that we need to keep the current behavior around, at least for situations where the generic parameters cannot otherwise be inferred.

However, I still maintain that when those parameters can be inferred, they should be inferred just as they would anywhere else.

Does anyone see a compelling reason not to pursue a course of action whereby, inside a generic type, we allow the bare name of that type to participate in type inference for its generic parameters, with a lowest-priority fallback of using the existing behavior if no other solution is found?

I remain leery of this fallback thing because it doesn't actually describe what the type checker should do in practice. What if the type name is mentioned twice in the same expression? Do you apply the fallback to one of them but not the other and try again? Or both of them at once? (I bet I can contrive a sufficiently bad example such that these two choices give different answers, but I'm also pretty sure that won't ever come up in real code. The point is that "as a fallback" isn't an implementation plan.)

2 Likes

Well, we can already use a bare generic type repeatedly in the same expression, and each will infer its generic arguments independently:

let tuple = (Optional(1), Optional("a"))
print(type(of: tuple))    // (Optional<Int>, Optional<String>)

I would expect that same behavior to work everywhere. And “as a fallback” here really means “Put this into the typechecker as a possibility, but make it rank really low so if any other possibility is valid, this one won’t be chosen.”

In your example, the two arguments are independent, but that isn't always true:

typealias UniformPair<Element> = (Element, Element)
let tuple: UniformPair = (Optional(1), Optional("a")) // error

I realize this isn't sufficiently contrived to run into the actual problem, but "rank it really low" doesn't handle the "what if two different things are ranked really low" problem.