Swift 6.3 compiler issue with Self?

Using the current Swift 6.3 compiler (TOT), I encounter an error while compiling the following code. Is it a clear misuse of the language, or is it a compiler-specific issue?

final class MyBootstrap {
}

func makeBootstrap() -> MyBootstrap {
  MyBootstrap()
    .if (true) {
      $0  // Error: Cannot convert value of type 'MyBootstrap' to closure result type 'Self'
    }
}

extension MyBootstrap {
  func `if`(_ conditional: Bool, option: (Self) -> Self) -> Self {
    if conditional {
      option(self)
    } else {
      self
    }
  }
}

This crashed the compiler, previously. What you see is an improvement.

The problem exists because you're using a class. The compiler has no way to represent a hierarchy-less class, even if it is only final. You must switch away from Self.

What I do when I absolutely need Self is I add a dummy protocol and hook the extension on that:

protocol DummyProtocol: AnyObject {
}
final class MyBootstrap: DummyProtocol {
}

func makeBootstrap() -> MyBootstrap {
	MyBootstrap()
		.if (true) {
			$0
		}
}

extension DummyProtocol where Self: MyBootstrap {
	func `if`(_ conditional: Bool, option: (Self) -> Self) -> Self {
		if conditional {
			option(self)
		} else {
			self
		}
	}
}

I never really caught on why this workaround is needed however. Feels like something is just missing in the compiler.

2 Likes

Yeah, it's one of the few parts I regularly miss from Obj-C. instancetype was a nice convenience in that language.

Yes, I would expect the error to occur within the extension itself, assuming the use of Self is prohibited. This situation creates an uneasy feeling because it suggests that such an error could unexpectedly appear in other parts of the code, when it is more likely due to a misunderstanding of the meaning of Self on my end. Hence my question, what is the meaning of Self in this extension?

1 Like

If you're going to do that, you can enforce

extension DummyProtocol where Self: MyBootstrap 

more accurately as

extension DummyProtocol where Self == MyBootstrap 

12 years on, with actors in the mix now setting a precedent for noninheritable reference types, it's probably time for some language improvements to better support noninheritable classes.

3 Likes

In the version I wrote, Self means the type of self which must be a type that conforms to DummyProtocol and also is MyBootstrap… which is equivalent to just saying the type is MyBootstrap. If MyBootstrap wasn’t final it could also represents its subclasses.

In the version without the protocol, I’d like to say it’s the same thing without the boilerplate of a pointless protocol, but the compiler is being difficult because without a protocol the internal representation must be different somehow. I don’t really understand it, it just feels to me like using Self with class types is half-broken. Clearly it could work as illustrated with the DummyProtocol example, but something is holding it back.

2 Likes

Self is a fun little piece of innocent-looking syntax that actually has three (?) fairly different meanings. I'm going to try to explain them and hope that I don't get any of them wrong:

  1. In a struct or enum context, Self is trivially equivalent to the name of the containing type. For example, in struct X { func f(_: Self) -> Self }, that's identical to if you had written func f(_: X) -> X, because that's the only possible meaning. That's the easy one.

  2. In a protocol context, Self is an implicit generic type parameter that refers to the concrete type that conforms to the protocol. That's how requirements like Equatable.== work: it's defined like this:

    protocol Equatable {
      func == (lhs: Self, rhs: Self) -> Bool
    }
    

    which means that the method in the conforming type takes two parameters whose types are itself.

  3. In a class context, Self is a special construction inside the compiler called "dynamic Self", where it means "this class or a subclass of it, depending on the concrete type of the receiver". This is actually pretty close to Objective-C's instancetype that was mentioned above, and it's similar in meaning to the protocol one except that it's not a generic type parameter, so it's more limited. Specifically, because of covariance, Self is only permitted in classes in a return type position (result of a function, or the type of a read-only subscript or read-only property). You can write this:

    class C {
      func f() -> Self
    }
    

    but the compiler explicitly bans this:

    class C {
      func f(_ x: Self)
      // ^ error: covariant 'Self' or 'Self?' can only appear as the type
      //   of a property, subscript or method result; did you mean 'C'?
    }
    

So what's going on with your example? I'm not entirely sure, but I wonder if the compiler is just failing to diagnose the use of Self when it's inside a function type parameter? I'm not sure if that's meant to be allowed.

@Slava_Pestov would probably have a better idea what's happening here.

12 Likes

Many thanks, Tony. Your explanation certainly helps me see things more clearly.

I'm not sure. Self and classes interact a little oddly. But I suspect this code should be legal.

PS. I don't think this code should be crashing the compiler. Which it seems to be doing in all prior versions of Swift.

EDIT: It works if you make Self a type alias for MyBootstrap.

final class MyBootstrap {
  // final really ought to do this automatically.
  typealias `Self` = MyBootstrap
}

func makeBootstrap() -> MyBootstrap {
  MyBootstrap()
    .if(true) {
      $0
    }
}

extension MyBootstrap {
  func `if`(_ conditional: Bool, option: (Self) -> Self) -> Self {
    if conditional {
      option(self)
    } else {
      self
    }
  }
}
2 Likes

Eh, this doesn't so much "fix" it as it just shadows Self as an entirely new name, and the fact that the compiler seems to be inconsistent about where it requires backticks is extra unfortunate. But you're just not working with Self anymore—it's equivalent to this:

final class MyBootstrap {
  typealias Blarf = MyBootstrap
}

func makeBootstrap() -> MyBootstrap {
  MyBootstrap().if(true) { $0 }
}

extension MyBootstrap {
  func `if`(_ conditional: Bool, option: (Blarf) -> Blarf) -> Blarf {
    ...
  }
}
1 Like