Abstract class in Swift?

No, This is unlike swift that we all love.

Could you be more specific? What about the idea do you dislike, and why? Is it an ergonomic problem that can be solved with better syntax, or is the concept itself somehow ineffective, unintuitive, impossible to implement, or otherwise flawed? What's an alternative approach that solves the problem?

I agree that the idea as initially presented is not production-ready, but it looks to me like a step towards solving some important problems. In order to reach those solutions the idea must be refined by in-depth discussion and debate, and a subjective, insubstantial dismissal does not serve to encourage that thorough discussion.

3 Likes

I’d like to add that a similar idea was presented during the pitch/review of the Identifiable protocol.

The protocol proposed an id attribute and many people had concerns about hijacking such a common variable name with very specific semantics that there was no way they could guarantee were met by all types that used an id property at the time. It was suggested that a property wrapper @implements could be used so that developers not able to or not wanting to refactor large codebases to meet the new protocols semantic requirements could still adopt the new protocol anyways, albeit by using a different property than id. In that thread the idea was widely accepted as very beneficial and potentially necessary in the future as more protocols are added to the stdlib.

1 Like

Interesting...some of that is possible already by overriding get/set. I've also sometimes used a PropertyComparable protocol for Models with a `SortOrder.

I like the idea but I'm unsure a Property Wrapper is right mechanism. E.g. if there was a public API that made available the mechanism used to synthesize conformances similar to Codable. Or something similar to a PropertyWrapper that fully included it's implementation in the enclosing Type rather than encapsulating it within it's own Type.

1 Like

So apparently I misspoke. The @_implements attribute already exists (albeit underscored) and was brought up during the Identifiable review.

There was additional later discussion about using KeyPaths as an alternative way for accomplishing the same thing which eventually led back to the feasibility of using @_implements.

I agree, Property Wrappers aren't the right tool here. IMHO, The entire value of a composition-based approach similar to the one I proposed is specifically that the property providing the protocol implementation is not part of the conforming type's API.

In fact, where I've used this pattern by manually writing all the boilerplate (pass-throughs for each function in the protocol, and get/set property pass-throughs for each property :pensive:), the type that was providing the implementation was a private property on the various outer types.

Do we still need abstract classes if we get support for object associated storage from extensions?

The only difference between abstract classes and protocols with AnyObject constraint, is that protocols can‘t have (private) stored properties. Even abstract sub-classes can be emulated with protocols, because we now have a superclass constraint. So if we get associated stored properties in extensions I think we can solve all the tasks an abstract class would solve but with a protocol instead.

As I mentioned up thread, you can’t call the protocol extensions default implementation when implementing it in a concrete class. Allowing storage to be declared would be a welcome step in the right direction to avoid boilerplate in a lot of classes.

I don’t think an abstract class has to be the solution, but it would be nice to find a way to get this behavior.

Yeah that's another unsolved problem in Swift. I always wanted something like default.method() similar to super.method().

2 Likes

This is not true. Classes can be generic and protocols cannot. Protocols with associated types cannot be used as existentials (yet). So there are other important differences. In fact, my primary use case for inheritance is to have a base class with "abstract" methods (the base class is sometimes, but not always, an "abstract" class). I have only rarely used this pattern to associate storage with the abstraction and most of the time when I have it has not been a necessary part of the design.

1 Like

Is that really so? One issue I run into is when I want an entire inheritance tree to conform to some protocol, but the abstract base class doesn't supply all of the conforming methods — implementation of those methods is dispersed to subclass overrides.

That implies, of course, that the abstract base class cannot be instantiated (it conforms, but doesn't implement the required methods). It also has the consequence that the abstract base class can serve as a (somewhat) type-erased type for subclass instances.

Both of those happy outcomes can't be achieved by replacing the abstract base class with a "class behavior" protocol — without additional hackery, usually involving a lot of "fatalError" calls.

Correct, I totally forgot about generics.

I didn't meant to say exactly that, but rather that protocols are still missing some core and convenient features as for today, but when those are added, do we still need abstract for classes at all?

To sum up a few things:

  • object associated stored properties from an extension (maybe also for value types, but that's a whole discussion of its own)
  • generic protocols (may or may not happen - it would however solve the 'abstract generic base class' problem mentioned upthread and some other topic unrelated issues such a type nesting)
  • full control of dispatch & an ability to explicitly access default implementation (e.g. default.method())
  • PAT's as existentials

Do we still miss anything that abstract class would be able to do, but a protocol with all these features wouldn't be able to?

To me it feels like the enhancements we could make to protocols completely outweigh the seemingly 'temporarily workaround' solution of abstract classes. In other words, protocols are almost there to replace that whole pattern.

Please feel free to disagree with me or correct anything that I might have said wrong. ;)

2 Likes

I totally agree all of those features are very needed and welcome additions that I believe swift will have in the future. However, I'm still having a hard time to understand how those features would solve the following:

protocol Section {
    associatedtype Item
    var items: [Item] { get }
}

abstract class TableViewController<S: Section>: UIViewController, UITableViewDataSource {
    let tableView = UITableView()
    let sections: [S]

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return sections.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections[section].items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        self.tableView(tableView, cellForItem: sections[indexPath.section].items[indexPath.row], at: indexPath)
    }


    abstract func tableView(_ tableView: UITableView, cellForItem item: S.Item, at indexPath: IndexPath) -> UITableViewCell

    abstract func registerCells() {}
}

class DerivedTableViewController: TableViewController<ConcreteSection> {
    // It would be awesome if the compiler told me that I forgot implementing
    // the abstract methods
}

This would avoid a lot of fatalErrors or abstractMethod Never returning methods that will explode only at runtime.

Could you show me how you would use the features you mentioned to solve this kind of problem? Maybe I already can employ some of your ideas right now and improve my code base.

2 Likes

Actually it's not that hard and it really depends on what existential you want to use in case if you decide to use the pseudo abstract type as a base.

Solution #1 under the assumption that we get the previously mentioned features including generic protocls:

protocol Section {
  associatedtype Item
  var items: [Item] { get }
}

protocol TableViewController<S>: UIViewController, UITableViewDataSource where S: Section {
  // customization points with default implementation below
  func numberOfSections(in tableView: UITableView) -> Int
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

  // protocol requirements 
  func tableView(_ tableView: UITableView, cellForItem item: S.Item, at indexPath: IndexPath) -> UITableViewCell
  func registerCells()
}

extension TableViewController {
  // simple strawman-syntax for associated stored properties,
  // which must have a default value!
  let tableView = UITableView()
  var sections: [S] = []
  // the original example missed an init because otherwise `section` wouldn't be initialized

  // ⚠️ this is not possible with protocols, we cannot override anything from the super class
  // constraint, but it could be a small price to pay
  // override func viewDidLoad() {
  //   super.viewDidLoad()
  //   tableView.dataSource = self
  // }

  // Workaround for this issue ☝️
  func setupDataSource() {
    tableView.dataSource = self
  }

  // default implementation of some `UITableViewDataSource` requirements
  func numberOfSections(in tableView: UITableView) -> Int {
    sections.count
  }

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    sections[section].items.count
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    self.tableView(tableView, cellForItem: sections[indexPath.section].items[indexPath.row], at: indexPath)
  }
}

class DerivedTableViewController: UIViewController, TableViewController<ConcreteSection> {
  // compiler will force you to implement protocol requirements, which are basically
  // like abstract members of an abstract class

  override func viewDidLoad() {
    super.viewDidLoad()
    setupDataSource()
  }
}

// later somewhere in the code:
var controllers: TableViewController<ConcreteSection> = ...

Solution #2 without generic protocols but with a PAT instead, which requres generalized existentials or open existentials:

protocol Section {
  associatedtype Item
  var items: [Item] { get }
}

protocol TableViewController: UIViewController, UITableViewDataSource {
  associatedtype S: Section

  // customization points with default implementation below
  func numberOfSections(in tableView: UITableView) -> Int
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

  // protocol requirements 
  func tableView(_ tableView: UITableView, cellForItem item: S.Item, at indexPath: IndexPath) -> UITableViewCell
  func registerCells()
}

extension TableViewController {
  // simple strawman-syntax for associated stored properties,
  // which must have a default value!
  let tableView = UITableView()
  var sections: [S] = []
  // the original example missed an init because otherwise `section` wouldn't be initialized

  // ⚠️ this is not possible with protocols, we cannot override anything from the super class
  // constraint, but it could be a small price to pay
  // override func viewDidLoad() {
  //   super.viewDidLoad()
  //   tableView.dataSource = self
  // }

  // Workaround for this issue ☝️
  func setupDataSource() {
    tableView.dataSource = self
  }

  // default implementation of some `UITableViewDataSource` requirements
  func numberOfSections(in tableView: UITableView) -> Int {
    sections.count
  }

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    sections[section].items.count
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    self.tableView(tableView, cellForItem: sections[indexPath.section].items[indexPath.row], at: indexPath)
  }
}

class DerivedTableViewController: UIViewController, TableViewController {
  typealias S = ConcreteSection

  // compiler will force you to implement protocol requirements, which are basically
  // like abstract members of an abstract class

  override func viewDidLoad() {
    super.viewDidLoad()
    setupDataSource()
  }
}

// later somewhere in the code (borrowed the syntax from https://forums.swift.org/t/improving-the-ui-of-generics/22814):
var controllers: any TableViewController<.S == ConcreteSection> = ...

This is of course my personal re-interpretation of the given problem solved with protocols and some of its missing features.

I don't really see the advantage in trying to bend or adapt protocols to something which can much more clearly be expressed as abstract classes. Sure we could add a bunch of features to protocols that allow them to completely replace the need for abstract classes, but what's the point? Isn't it better to keep protocols protocols and then have abstract classes as another tool in our belt to use.

Protocols could be changed so they can replace classes altogether and completely remove the class keyword, doesn't mean it should be done.

5 Likes

I think the point is that protocol features can be taken advantage of by both reference and value types whereas abstract classes only support classes. Any of us trying to avoid reference types (where possible and semantically appropriate) do still come up against situations where the protocol features described would be useful outside the context of reference types.

4 Likes

I don't think that we're trying to slam all kind of features into protocols. I think most of the mentioned features are still on the roadmap of Swift evolution. Unlike abstract keyword, the independent protocol features grant you far more flexibility and expressiveness in terms of library design. And if combined, you basically get the same set of features as an abstract keyword would give you, which from my perspective makes it totally redundant.

I'd argue that even with all the additions to protocols (which some day may happen) having an abstract class in some places is just semantically better. It will read better to the humans reading and writing the code.

2 Likes

Thanks for explaining me your ideas @DevAndArtist =]

I think those protocol and generics enhancements would be very nice to have and would improve the language expressiveness.

Now, with a more real world example in mind, I clearly noticed that the protocol based approach has some disadvantages when compared to abstract classes like the lack of class overrides in viewDidLoad example that can become a problem if we call a lot of methods in this override, and IMHO adding storage to extensions could be harmful and create bugs that we don't have today.

OTOH this approach is more general and would help to solve a lot of problems we face today without adding a lot of new concepts and keywords.

Just hit today the lack of abstract classes myself. I'd would be quite happy with a workaround similar to what topic starter wrote, but I also need to call abstract method from concrete method.

Ok, we could do (self as! AbstractMethodRequirementsForC).abstractMethod(). That's a bit ugly, but works.

Except that is does not because, my abstract base class is also generic. And this means that my protocol gets associated types, and cannot be use as a type anymore.

In the end I was able to push some of the code outside of the class, got rid of the calling abstract method from concrete method, and kept generic base class and a protocol with associated types. This forces all the code that uses this class to be generic, rather then working with existential types, but for my case I was lucky enough to have an existing generic class that I could use to stop propagating generics though the codebase.

But what if we could make it actually work?

Step 1. Enable any protocol to be used as type - see some discussion in Improving the UI of generics
Step 2. Enable generic type aliases with constraints, also mentioned there.
Step 3. And the final bit, that's a new suggestion from me - where Self: AbstractMethodRequirementsForC

So it would look like:

// Having two definitions requires more code, but in a way it makes code cleaner, but separating public interface from implementation.
// Even in languages with built-in support for abstract classes, it is still a good practise to have a separate protocol
protocol CProtocol {
    associatedtype U: SomeConstraint
    func abstractMethod(_: U) -> U
}

// That's a 100% boilerplate code that should be eliminated somehow
typealias C<T> = CProtocol where U == T

// Typically any implementation of C fits your needs, even if it does not inherit implementation from AbstractC
func use<T>(x: C<T>) {}

// ... but not always
func useAndRequireImplementation<T>(x: AbstractC<T>) {}

class AbstractC<T: SomeConstraint> where Self: C<T>  {
    var x: T

    init(_ x: T) {
        self.x = x
    }

    func concreteMethod() {
        // the `where` clause above enables calling methods from AbstractMethodRequirementsForC even though it is not implemented here
        self.x = self.abstractMethod(self.x)
    }
}

 // Error: BarDerived  cannot inherit from AbstractC because it does not conform to C<String>
class BadDerived: AbstractC<String> {}

// OK: forward protocol requirements
class AbstractDerived<T>: AbstractC<Array<T>> where Self: C<Array<T>> {}

// OK: protocol requirement satisfied
class ConcrecteC: AbstractC <String>,  C<T> {
    func abstractMethod(_ x: String) -> String {
        return x + x
    }

   // override works
   override func concreteMethod() -> String {
       // super works
       let s = super.concreteMethod()
       return "\"\(s)\""
   }
}

use(ConcreteC())

Regarding the problems described in deferral rationale:

An abstract class cannot be instanciated.

This can be prevented in cases when AbstractClass(...) is statically spelled, but the behavior of abstract class metatypes must be specified. Is let classObject: AbstractClass.Type = AbstractClass.self allowed? Does let classObject = _typeByName("Module.AbstractClass") work? If not, then the abstract class object cannot be used for metaprogramming or generic argument binding purposes. If it can be, then there's a static safety hole, since classObject.init(requiredInitializer:) would have to dynamically fail if classObject refers to an abstract class metatype.

For the let classObject: AbstractClass.Type = AbstractClass.self, I think it should work, but you should not be able to instantiate anything using that metatype. To instantiate something, you need a metatype of type (AbstractClass & Protocol).Type. Base.self does not type check against that type, but ConcreteSubclass.self does.

Similarly, _typeByName("Module.AbstractClass") would work and would return Module.AbstractClass.self, but trying to cast that to the (Module.AbstractClass & Module. Protocol).Type would fail.

Terms of Service

Privacy Policy

Cookie Policy