Possibility to makes functions `required`

Introduction

In swift we are unable to tag a func as required like we can do with initializers.
This is annoying and make sometimes our code not completely developer-friendly.

What I have

class Building {

  var defaultLights: Lights!

  func makeItBeautiful() {
      // do some stuff here
      turnOnLights()
  }

  func turnOnLights() {
      // turn on default lights....
      defaultLights.turnOn()
      turnOnCustomLights()
  }

  func turnOnCustomLights() {
      fatalError("This method must be overriden") // πŸ€•
  }
}

class BigBuilding: Building {

  var customLights: Lights!

  // and even more custom element from `BigBuilding`

  override func turnOnCustomLights() {
      // turn on custom lights
      customLights.turnOn()
  }
}

In StackOverFlow we can read thousands of topics talking about this kind of case, all solved with fatalError(_:) solution.

What we can do using protocols

protocol CustomizableBuilding {
    func turnOnCustomLights()
}

class Building: NSObject {

    var defaultLights: Lights!

    func makeItBeautiful() {
        // do some stuff here
        turnOnLights()
    }

    func turnOnLights() {
        // turn on default lights....
        defaultLights.turnOn()
        if self.conforms(to: CustomizableBuilding) { // 😱
            (self as! Customizable).turnOnCustomLights() // 🀒
        }
    }
}

class BigBuilding: Building, CustomizableBuilding {

    var customLights: Lights!

    func turnOnCustomLights() {
        // turn on custom lights
        customLights.turnOn()
    }
}

In both ways, inheritage is not self-sufficient, we need to add some hack to alert the developer that he forgot some piece of code.

On the first solution we have to think about overriding the method turnOnCustomLights.

On the second solution, we have to think about implementing the protocol CustomizableBuilding each time we inherit from Building.

This is not completely developer friendly, we can forget it.

Alternative way using protocols


protocol Building {
  var defaultLights: Lights!
  func makeItBeautiful()
  func turnOnLights()
  func turnOnCustomLights()
}

extension {
  func makeItBeautiful() {
    updateUI()
  }
  func turnOnLights() {
      turnOnCustomLights()
  }
}

Assuming that Building won't be used alone, protocol is the good way.

But Imagine that I would like to use Building as a class and giving me the opportunity to subclass it if needed, you can do it in better way by adding the required keyword as you will see below.

Proposed solution

class Building {

    var defaultLights: Lights!

    func makeItBeautiful() {
        // do some stuff here
        turnOnLights()
    }

    func turnOnLights() {
        // updateUI....
        // defaultElement
        turnOnCustomLights()
    }

    required func turnOnCustomLights() { /* no custom lights with that building */ }
}

class BigBuilding: Building { // πŸ”΄ error: You must implement `updateCustomUI()`

    var customLights: Lights!

}

var 🏠 = Building()
🏠.makeItBeautiful() // turn on default lights

var 🏒 = BigBuilding()
🏒.makeItBeautiful() // turn on default lights and custom lights even simpler that before πŸ’ͺ 

Conclusion

Adding required keyword to all methods can bring more readability for the developer.

It will automatically raise an error on the subclass.

It also gives use the way to use the class without subclassing it. This is not an abstract class.

I personally would find that this addition makes the language more complex and confusing without bringing any benefits. As you've mentioned in the post, there's already a great way to implement this with protocols. In fact, when you find yourself writing code like fatalError("This method must be overriden"), this is a great signal that you should use protocols (and protocol extensions) instead of class hierarchies.

1 Like

I would like to elaborate on this a bit. When you're saying "use Building as a class" this already opens a big can of worms. If Swift had something like "abstract classes":

  1. this would already prevent anyone from creating instances of Building as it wouldn't have a valid turnOnCustomLights implementation. So why require Building to be a class in the first place?

  2. if you'd like to reuse "lights-related" behavior in other types (say structs and enums), you wouldn't be able to because only classes can inherit abstract classes.

With protocols you already have (1) working well: you can't create "an instance of a protocol", you can only create an instance of a conforming type directly. (2) also works with protocols perfectly well. I don't think there's a use case here to "use Building as a class and giving me the opportunity to subclass it if needed". You can already add conformance to a protocol, which would give the "lights-related" behavior to the conforming type. Even better, you can conform to multiple protocols to inherit multiple behaviors (and possibly customize at customization points), which isn't available with classes in a sensible way due to problems with multiple inheritance.

Sounds like you’re describing abstract methods, which was reviewed in SE-0026 and deferred. I recommend looking at and synthesizing the interesting discussion that has already taken place about that topic, which you can find by searching these forums.

3 Likes

I second this. In my code-base I have classes that are faked abstract classes, other modules such as RxSwift use faked abstract classes, @Nevin used also a fake abstract class in his proof-of-concept implementation here: Emulating variadic generics in Swift

2 Likes

Could you please provide more concrete examples?

What I see in the "variadic generics" post is that a generic class is used to work around the lack of generalized existentials (maybe it could be implemented with opaque result types too?). This means generalized existentials and opaque types are definitely worth adding and there's a strong consensus about that. And when that's implemented I personally don't see a single use case for abstract classes to be added on top of that.

Well the use-case for abstract classes is in general if you allow subclasses outside from your module. Right now you will create type members that trap and the user has to find out that either by documentation, by reading the original source code if any of these two things are available or the most annoying way by writing a subclass without overriding the abstract members and then trap at runtime.

RxSwift don't allow you to create sub-types of Observable so they are 'kind of' safe, if they don't forget to implement all the abstract members for new sub-types.

Here are a few more usages of rxAbastractMethod.

Abstract classes compared to protocols allow you to implement private mutable storage which you can use to implement the non-abstract parts of the type. That is still very useful, especially with the compilers help when you would create a sub-type, it will be handy when the complier will tell you are required to implement.


To sum up. I think it's a useful pattern, but it's a low priority feature that has other workarounds for now.

I'd just point out that the standard library also uses abstract base classes - for KeyPaths and AnyCollection boxes.

So even the standard library authors encounter programming challenges in Swift which are best solved with this pattern.

8 Likes

I completely agree, that's a great signal. But I think it's even better if the compiler alert us before the app crashs at the runtime.

Absolutely, but is there anything currently wrong with the compiler alerting you that you don't have a protocol conformance fully implemented? We don't need any changes to the languages to get that right now.

There are use-cases where a protocol cannot be the super-type to represent such 'abstractness', because you may require generic types, which in case of a protocol will be an associatedtype that won't allow you to up-cast, this issue does not exist with abstract types. And as already mentioned above, you could have private storage in an abstract type, which is impossible for protocols and is not yet even decided if it ever will be allowed.

3 Likes

Right, but it's known that the current implementation of AnyCollection is a workaround for the lack of generalized existentials. I assume this is the case for KeyPath hierarchy too, but probably designers of that class hierarchy could clarify that.

I think we need to highlight that there are two completely different cases here:

  1. Non-generic behavior inheritance with an example given by the OP.

Solved perfectly well by protocols and protocol extensions as they are available now. In addition protocol extensions provide support for multiple inheritance that wouldn't be available with abstract classes unless you're willing to do what C++ has done with multiple inheritance.

  1. Generic behavior inheritance with the examples that followed.

These can be implemented with PATs right now, but there are known issues with PATs, which people work around with the examples like above. This includes the standard library and many many more examples. As far as I understand there's a concrete plan to make those workarounds unnecessary with opaque types first and after that the remaining use cases could be resolved with generalized existentials.

The problem with "abstract classes" as they're presented right now here is that they're equivalent to generic protocols. And if Generics Manifesto is to be believed, we're unlikely to ever get them implemented in Swift:

More importantly, modeling Sequence with generic parameters rather than associated types is tantalizing but wrong: you don't want a type conforming to Sequence in multiple ways, or (among other things) your for..in loops stop working, and you lose the ability to dynamically cast down to an existential Sequence without binding the Element type (again, see "Generalized existentials"). Use cases similar to the ConstructibleFromValue protocol above seem too few to justify the potential for confusion between associated types and generic parameters of protocols; we're better off not having the latter.

And personally I'm perfectly happy with that. It is known that generic protocols (and corollary "abstract classes" as presented here with equivalent features) are flawed as they lead to unnecessary blow up of generic function signatures as was shown here very well by @algal. As shown at the link, Java has chosen abstract classes instead of PATs, which requires them to have 8 generic parameters in functions that could have much less (3 or so I guess as was suggested in that talk?) if those were PATs.

Yes, PATs in Swift currently have flaws too. But at least we have a clear path forward and a general idea on how to fix them. Adding abstract classes/generic protocols would only make things more complicated.

It seems to me that your vision of opaque types is more like a magic existential alike type, which would be not correct. opaque type still require a concrete type to exist at the palm of the implementor, but is then simply hidden for from the user.

One thing that classes can do that protocols can't is storage. This of course until we have mixins in the language :)

1 Like

I'm sorry if you've got that impression from what I've posted. I'm not claiming that opaque types are more like existential types, I'm only saying that some of the workarounds for current limitations of PATs would be resolved by opaque types. And if some could be resolved with abstract classes/generic protocols, there would be less reasons to add those to the language in the first place.

1 Like

I personally dislike such a point of view, because then we could stop adding any new feature to the language because it will make things more complex. Don't get me wrong, this is just critics. generic protocols serve a completely different problem area which isn't easy to achieve with PATs. The common rejection of that feature as far as I remember comes from the complexity of the implementation (sure this will always be true) and that at this point of time many developers will most likely fall into the trap and use generic protocols instead of PATs where PATs would be the correct solution but generic protocols would 'seem' to be more convenient at first glance. Other than that generic protocols would be super useful for more experienced Swift developers because it will unleash a new degree of flexibility in expressiveness of the API design.

I think @regexident can write you a whole book about generic protocols. ;)

Sorry, I'm not sure what you mean here by "impossible", I've previously had use cases like this and protocols were a perfect fit:

public struct Storage<T> {
  private var storage: [T]
}

protocol Container {
  associatedtype Contained
  var storage: Storage<Contained> { get }

  func someAlgorithmThatRequiresStorage() -> Int
}

This is a contrived example to make it fit, but I hope it illustrates the idea.

First, you cannot mutate that storage, at least in your example. Second you have to expose it if your protocol is public. That problem does not exist in case of abstract types.

1 Like

Not sure if this is the updated gist or not, cc @regexident:

But this topic seems to derail into a different topic, so we should stay focused around abstract types rather start talking about generic protocols.

Again, I hope I can make this more clear: I'm not against improving the language, in fact I'm for it as much as possible as I think Swift is currently too limited for a lot of use cases. What I mean here is the unnecessary complexity. PATs and generalized existentials introduce complexity. Generic protocols introduce even more complexity. Why have 2 complex features in the language that overlap so much? Especially as one as indicated by the Core Team as a clear way to move forward and the other one is clearly stated as "unlikely to be implemented" in the compiler documentation?

And this is the point that I'm trying to see concrete examples for. What so far has been listed above could be resolved with improved PATs. I look forward to seeing examples that wouldn't be possible to implement with PATs, generalized existentials and opaque result types, but would only be possible to implement with what's been originally described here as "abstract classes".

I'm not sure what you mean here by "abstract types", could you please clarify? If we keep discussing "abstract classes" as was described by OP, you have the same problem with those: you need to make storage public on the superclass for subclasses to be able to access it.

Terms of Service

Privacy Policy

Cookie Policy