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

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.

Right, String does have value semantics. But I intended this as a general example and the compiler has no way to know which types have value semantics in general.

If running arbitrary code after the call to super does not cause any problem, then perhaps that means superrequired(postfix) does not solve any problem. Clearly, both the problem it attempts to solve and its intended semantics need clarification.

Good thinking. However this doesn't account for the requirement to actually call super, so it has to be combined with a super attribute.

Now that you mentioned it,

presuper func foo() {}
postsuper func foo() {}
super func foo() {}

without additional parameter attributes looks a bit better.

@michelf
It is okay to treat it as an error if the the requirements of an attribute aren't fulfilled. For instance, if a class has a method marked super, but you don't call super when overriding it.

1 Like

*If* there is a compelling reason to introduce the ability to enforce “must call super first / last”, then we should probably take a cue from “deinit” and make such calls automatic.

As in, when a base class declares a method as “subclass implementations must call super exactly once at the beginning”, the compiler automatically calls super and it is an error to do so manually.

For example:

class Base {
  @superAutomaticallyCalledFirstStrawmanSyntax
  func foo() {
    print("Base foo")
  }
}

class Sub: Base {
  override func foo() {
    print("Sub foo")
  }
}

let x = Sub()
x.foo()
// Prints:
// Base foo
// Sub foo
3 Likes

My apologies, I was unclear.

Let’s consider the possible effects of newString deallocating.

  • if newString going away has an effect, then super stored a weak or unsafe reference. This is orthogonal to requiring a call to super.

  • if super had work to do using newString after it was called… then the requirement that it be called at the end was a lie. I argue that this is not a problem with the feature but with the use of the feature.

Running arbitrary code after he call to super has no effect on memory management, was my assertion.

TJ

The problem with this aspect of this thread is that "first" and "last" are not syntactical requirements on the code, but just hand-wavey shorthand for saying that super must be called before/after self messes with the things super messes with. It is generally unspecified exactly what things super messes with, and there's literally no way of knowing for sure (unless you can go read super's source code).

An obvious case where you necessarily put code before a "required-first" call to super is when there is a parameter to be passed upwards, and you have to calculate the value to pass. An obvious case where you necessarily put code after a "required-last" call to super is when you want to asynchronously dispatch a closure that relies on whatever super set up for you.

I don't see any way the compiler can enforce any valid rules about when super is invoked.

The requirement of calling super first or last implies that you shouldn't mess with the parameters of the overriden method that you later pass to the super implementation. Passing parameters to super that are different to those of the overriden method shouldn't be your everyday practice..

Nevertheless, this is a good point. Maybe, the proposal in question should be considered to apply only to void functions func foo() -> ()

Ashamed as I am to admit it, I'm sure I'm not the only person who's committed this sin. But, yes, this objection occurred to me after I posted. So:

An obvious case where you necessarily put code before a “required-first” call to super is when there is a parameter to be passed upwards, and you have to guard against certain values of the parameter (because they imply behaviors your subclass doesn't support).

I'm not ashamed to admit I've done that (I think).

Sounds like the definition of a creative person lol. I suppose we all've done it at least once.

It's unusual to assume a class doesn't support some behavior of its superclass though. An example would be intriguing.

I guess my concern is that this will add compile based checks that block workarounds, or creative uses of API that the API vendor never envisioned. Eg calling a private method which then calls super.

Requiring super could mean many things, as highlighted by some requiring it first or last. Isn’t it better to leave it to documentation than to try and force something on subclasses?

For example...

Say I had a method where the last action must be to call super, but I want to track state, and set a flag or print a log or call a method after super is called. Is that ok? The compiler says “requires super at end” but the real meaning is “requires super after all actions directly relater to this method”. Why is logging or state flags or calling tangentially related methods now blocked because the developer tagged their API in such a way?

I think we need to differentiate between guidance and requirement. Unfortunately it seems more and more we are ditching “guidance” under the guise of a “safety feature” and tying our own hands in the process.

I wouldn’t mind having something like the Clang Analyzer run through and determine if a codepath actually does end up calling super tho...

1 Like

I'll steal @ethanjdiamond's wizardly enum from another thread:

enum Attack {
	case orc(weapon: Weapon)
	case elf(spell: MagicSpell)
	case human(karateMove: KarateMove)
}
class Fighter {
	func deploy (attack: Attack) { switch attack { /* … */ } }
}
class Orc: Fighter {
	override func deploy (attack: Attack) {
		guard case .orc = attack else { fatalError ("Orcs aren't smart enough to do that") }
		super.deploy (attack: attack)
	}
}

Such a small example is a little thin (and there's no obvious super-first requirement here), but think of a class that can do wireless or cellular or Bluetooth networking, with a subclass that only supports wireless. Stuff like that.

I hate to be that guy, but all this talk about the problems around requiring super first or last has already been discussed at length in previous conversations that @DevAndArtist mentioned. Avoiding this sort of rehashing on the list is exactly why @DevAndArtist reminded participants to refer to those conversations first.

It seems you are right about the code related to super. As such, we could defer{...} what we want in case of a final super call, but we can't do much with a starting call. This suggests such strict requirements are unreasonable and, as @QuinceyMorris said, it is hard to believe there is a nice way for the compiler to correctly enforce these rules.

Looks like this thread reduces to whether we need an attribute for simply requiring super to be called.

And yet, here we are rehashing over the same stuff as if it is still a valid idea without the issues being adequately addressed. If we can't bring up the past, what's the point?

I think maybe I wasn't clear in stating my point, which was precisely that participants in this conversation aren't bringing up the past, in which (exactly as you say) valid points were raised. We discussed this issue in several other threads as well:

This forum is intended to be a place to make progress on Swift (in fact, it is essentially the only place), not merely to talk about hypothetical improvements. This requires volunteers to be willing to dedicate real time and effort in making insightful and timely contributions; such contributions are greatly disincentivized when they are simply forgotten in a handful of weeks or months. It's hard enough to attract good people to contribute their thoughts once and on time; no one wants to spend their spare time making the same points over and over again.

Making the historical archive of insight easily available for all comers was one of the reasons for moving to an easily searchable forum format. Now that we have this resource so that resurrecting an old issue is now extremely easy, it's incumbent on those who do so also to take the time to find existing conversations, then to familiarize themselves and acquaint the community with what's already been said. That way, topics keep their forward momentum rather than starting back at square zero.

When @DevAndArtist pointed out that this topic has already been discussed, it was precisely so that the next two dozen messages didn't have to rediscover points that have already been extensively and well explained in the past. Instead, your energies and that of all the participants here could have been focused on actually making forward movement on the issue.

4 Likes

I've looked through the various threads linked in this current thread, and here's my TL;DR of those conversations:

There is no agreement on any aspect of this proposal, and a general unwillingness to proceed.

The single idea that produced some enthusiasm (a couple of times) was having the compiler remind you in obvious cases like "viewDidLoad()". For that use case, here's a variation that AFAIK is not a rehash of what has gone before (at least, not to any significant degree):

  • Introduce 3 new keywords as method modifiers: necessary, unnecessary and satisfying (straw-man names, of course).

  • necessary: Indicates that an overrider of this method is expected to invoke super. Textual omission of the super invocation in the override produces a compiler warning.

  • unnecessary: Opposite of necessary, and the usual default, indicating no need for overrides to invoke super. Used explicitly to make an override of a necessary method unnecessary to the overrider's own overriders, otherwise the necessary modifier would be inherited.

  • satisfying: Used on an override to force it to be treated as meeting the necessary requirement of its super method, even when there's no super invocation textually apparent in its code (or when it chooses not to override, for twisted reasons of its own). This is basically just a warning suppressor.

However, given that the motivation for this is to get reminders in a lot of well-known APIs (Cocoa and Foundation APIs, mostly, I guess), and given that the chances of these existing APIs getting necessary annotations is probably about zero, I can't say I think that even such a relaxed approach is worth spending effort on.

Finally, a simpler approach might be just to encourage the documentation of override requirements in /// comments preceding the super method, which may be easily accessible to writers of override code. (They're only a option-click away in Xcode, for example.)

4 Likes

It looks like the rules are already implemented in SwiftLint, so the check is just a few configuration steps away for those who want it. We use the linter in production (as a part of the build) and are mostly happy with it.

-[UIView draw] 'does nothing' according to the documentation and subclasses don't have to call the base class implementation.
If you want to override draw, you must no be required to call the superclass implementation. You may want to customize a control look without having to pay the cost of drawing the original first and then overriding it with your custom drawing.

I would say omitting unnecessary as a default and leaving just the necessary modifier is the best option. A necessary method should be called chainwise no matter how branched out is an inheritance hierarchy, so we can leave it out formally here as well. As for satisfying, it sounds like spooky action, especially for Swift, considering it (a modifier) is meant to be used as a warning silencer.