Add ability to make a call to super a requirement in subclass overrides

Adding the ability to additionally specify superrequired(start | end) func foo(...) -> ... {...} makes sense as then everything would be checkable at compile time. Leaving out (start | end) would imply that it makes no difference where super is called as long as it is called at some point.

Another option could be:
superrequired(first | last) func foo(...) -> ... {...}

Please search the forum for this topic if you want to revive it, because it was already discussed before. Then summarize everything that already has been considered, so that we don't rehash it from zero.

2 Likes

Ok. I did try to search for the topic without success before. What is the topic where this was discussed?

I don't remember myself, but I just tried myself by searching for super and found this. You can do further research then. ;)

After skimming through the previous threads the proposal did not seem to make it to a formal review. Not sure why, I noticed no important objections. There were just good arguments against the before/after flags, since they would disallow fairly legitimate cases like logging or mangling the arguments before calling super. Also, the existing NS_REQUIRES_SUPER macro from the old world is worth mentioning in this context.

2 Likes

I too skimmed thru the previous threads thinking that there were legitimate arguments against the before/after flags, but I remain unconvinced that some form of superrequired would not be beneficial. Is not one of the main goals in Swift to catch as many errors/bugs in the compile stage? Leaving a call to super as only a matter of documentation does not seem in line with this philosophy to me.

Does anyone think that this would warrant a more rigorous summary of the previous threads and some further investigation/discussion?

It is a regression compared to the NS_REQUIRES_SUPER available for Objective-C this all things considered, no?

Thank you for reviving this everyone. I never understood why it was never actually reviewed. And at some point it was just closed as out of scope and I was unable to reopen it. I’d love to get a better explanation. I have found numerous issues with people making these mistakes in our codebase.

Is the final(ish) proposal draft to be seen somewhere? Can we revive it and try again? But:

The broader range of proposals for Swift 5 compared to Swift 4 incurs the risk of diluting the focus on ABI stability. To mitigate that risk, every evolution proposal will need a working implementation, with test cases, in order to be considered for review. An idea can be pitched and a proposal written prior to providing an implementation, but a pull request for a proposal will not be accepted for review until an implementation is available.

Here is the closed PR: https://github.com/apple/swift-evolution/pull/211

Hm, I see. It seems that this is out of scope for Swift 5, too, unless we can augment the proposal with a working implementation?

NS_REQUIRES_SUPER is a workaround for poorly design classes. A class that requires a method to call super should declare this method final and provide other hooks that don't have this requirement for subclasses.

5 Likes

Unfortunately these “poorly designed classes” are the bread & butter for many people working in Swift. Would it make sense to introduce rules for the UIViewController methods into a Swift linter instead of the Swift compiler? That would be at least some progress.

2 Likes

A final method with a separate hook scales poorly in deep class hierarchies. For example, it is unreasonable to require every subclass of UIView to define a unique hook for draw(_:).

5 Likes

Excellent point! I've always found that saying x is poor design, has more to do with the latest fad then actually solving programming problems (I'm not suggesting that there are no bad designs). There is no one technique that solves all problems and therefore supporting different techniques is a good thing at least in my opinion. Having a requiressuper formalizes the intent of the class designer and adds robustness to the codebase.

It's definitely a useful feature that addresses a real problem, at least in Cocoa: There are some methods calls that you have to forward when you override, and others that require you not to do so - and the compiler doesn't help you with those rules that are formulated in the documentation of UIViewController ;-)

A way to express exactly what you have to do when you override could also replace property observers, which imho are quite heavyweight (willSet, didSet, oldValue, newValue...) for such a small (albeit useful) feature.

Have you considered something like:

override func viewDidLoad() {
    willCall {
        // Do pre-super call stuff.
    }
    didCall {
        // Do post-super call stuff.
    }
}

This way you follow the willSet, didSet, ... that @Tino mentioned, but will maintain Swift-like nature without introducing additional attributes.

A subsequent thought is something like:

final(body) func viewDidLoad()

which would mark the method as not overridable in a sense that you can fully replace the body of it, but you can still override it with willCall and didCall. But that may be too wild...

2 Likes

I'm not convinced this is needed. If it gets added, I think it should be a warning, not an error, and should offers a way to silence the warning, similar to how ignoring a return value results in a warning today. But if it goes forward, here are some point that deserve more thought.


The ability to specify whether you want the call to super to happen at the end doesn't make sense to me. It's actually quite ambiguous what it means. Take this simple function:

func test(with string: String) {
    let newString = string + "some suffix"
    super.test(with: newString)
}

Now, you might think the last thing the function does is calling super.test(with: newString), but that is not actually the case. The last thing this function does is decrementing the reference counter for newString's storage, and possibly deallocating the storage.

In reality, it's pretty common to have some cleanup operations at the end of a function call. That cleanup can end up calling arbitrary code (think of defer or class's deinit). And while you could try to specify which kind of cleanup code is allowed and which isn't, I doubt it is possible define something useful.

You could disallow cleanup from happening after the supercall (which would also guaranty a tail call optimization), but then trivial code like the example above will not compile for reasons that could appear quite obscure.


Similarly, making it an error to not call the superclass's implementation will break patterns that rely on calling it indirectly from another function. That might seem a weird thing to do, but think about how you sometime wrap code in closures:

func moveUp() {
    UIView.animate(withDuration: 0.2) {
        super.moveUp()
    }
}

While you know the closure is called once and only once, the compiler doesn't. It will have to emit an error if the superclass implementation had this requirement about calling it in the overridden implementation.

And this is an interesting problem because of its similarity: the animate method must call the closure you pass to it exactly one time, much the same way you want the superclass' implementation of your method to be called exactly one time. Perhaps there is something to generalize here so it can apply to every case where a function must call another one exactly one time.

1 Like

Maybe I am just being pedantic but… this doesn't matter. The string has value semantics so it shouldn't be an issue. Even if it had reference semantics, it seems like this should not be an issue. If it does create a problem, I feel it indicates a different, deeper, issue with the workings of your type.