Abstract class in Swift?

What about multiple-inheritance? There are good reasons why few languages besides C++ support it.

Seems orthogonal to me, but I might be missing something. It is composition, not inheritance.

protocol A {
   var a: Int  // straw-man syntax, mimicking regular storage declarations.
}

protocol B {
  var a: Int
}

struct S: A, B {
}

What is S.a? Are there two fields? How would one access one or the other? If there's one, how would one cope with situations where the semantics of the protocol are different? Suck it up and not conform to both protocols?

1 Like

Oh, name collisions not multiple-inheritance. That's a more general Swift issue that already arises with existing features (two protocols with the same method signature, etc.) and doesn't strike me as being uniquely bad for protocol properties. There needs to be a general mechanism for resolving these existing conflicts, which properties could presumably use also. There are some steps towards this already, e.g. @_implements, and various discussions about qualifying names.

2 Likes

The difference is now it's relatively rare for real conflicts to emerge and they can usually be avoided by implementing the property/method in the adhering type. Adding additional abilities will only make those already complex structures more complex and make them harder to reason about.

Those types of complex structures are also rare because they're not easy to use. Feature not a bug? :smile:

Objective-C had the same problem, since like Swift it also oddly skimped on robust syntax for namespacing & qualifying names, particularly with delegate protocols. The "solution" in Objective-C was the convention that protocol method names start with the name of the protocol. I'd like to see that avoided in Swift, by instead having a mechanism to disambiguate when needed via an optional namespace selector, e.g. self.B::a (or better).

1 Like

I totally get that we don’t want to end up like C++, and generally get having opinionated language. But realistically this is just one keyword that fits in a place another keyword does (final), so practically speaking “don’t like it, don’t use it” pretty much works here. (I may have a got a bit over the top tbh :see_no_evil:)

All of these already exist in concrete classes. So, if we improve how concrete classes work with protocols, we would not need to add this to protocols. What do you think of class Base where Self: AbstractMethodsProtocol syntax, I've described here?

1 Like

Apologies, but that seems to be framed from a “let’s not do abstract classes” starting point and looking to add features to protocols to avoid having need for abstract classes as a solution.
I am not sure protocols should have storage either (few more steps and protocols are essentially classes by another name :)), but then I have never been sold on default implementation in interfaces/protocols as I see them as pure contract devoid of implementation...

2 Likes

I've been trying to come up with some useful response to this thread for a while, but it seems that I can't and I'm thinking about why. In my years of experience with Objective-C I was an OOP enthusiast, and never for once I felt the need to use an "abstract" class: I knew the concept existed, but I guess I was immediately sold by the mantra "prefer composition over inheritance", thus I avoided inheritance (of implementation) from the start, and I can't remember a single case where I felt the need to use an abstract class.

So I guess I'm not really the target of this discussion, but I know my share of OOP and maybe I can still add something to it. I'll refrain from the usage of the word "antipattern", which I consider meaningless: I rarely find truly rational arguments in favor of a coding style (it's almost always like "there are use cases for it" or "if used properly"), and the word "antipattern" sounds like an objective judgement of a technique, with no defined criteria to support that objectivity.

Also, a quick note: I think it's useful to ground this discussion in programming theory, because adding a feature to a language requires a huge effort, and a solid language must be opinionated in regards to its features, otherwise anything would go, and I'm sure many people can find good usages of goto if they put their mind to it.

First of all, abstract class != OOP: object-oriented programming is unfortunately a rather vague concept, but I'm aware that there's a whole school of OOP that strongly discourages the use of abstract classes and inheritance (of implementation) in general. Thus, I don't think it's useful to frame this discussion in terms of Swift support for object-orientation.

Then I'd say, from what I can read, that the things that are requested and missing in Swift when implementing an abstract interface are:

  1. inheritance of stored properties;
  2. the possibility of calling super from an overridden method implementation;

(From now on, when I say "inherited" I mean "inherited from an abstract interface", and when I say "inheritance" I mean "implementation inheritance")

About 1. I can say that I found myself wanting this a few times, but the "raw" solution to it (that is, adding the property to the concrete type conforming to the abstract interface) is simple enough that I don't see it as a big problem. Also, if there's a lot properties that are supposed to be inherited, it's probably better to define a struct for them with a name that describes what that collection of properties represents. I strongly oppose the idea of adding "stored properties" to protocols, I think it would be a terrible idea, and I'd be happy to discuss why someplace else, if someone is interested (it's OT here). But adding a huge feature like an abstract class just for this would be overkill. Also, abstract class would be an imperfect solution for Swift because the language strongly encourages the usage of structs that, of course, cannot inherit from classes; seriously, the usage of structs when passing around information and data is one of the few things that Swift truly encourages: if you're using "value objects" defined with immutable classes for this use case, you're doing it wrong.

Another case for 1. (I think it's related to example @phlegmaticprogrammer brought to the table) is when you actually need a class because the state is mutable and must be kept track of, but the class should declare some abstract methods to be implemented by subclasses. This is 99% obtained by passing those methods' implementation in the constructor, which also makes it super easy to test. Seriously, think about it: the class needs the implementation for those methods to be provided by something else, and the easiest, most direct way of doing that is by passing those implementations explicitly. The functions that implement those methods can be grouped in a logical struct that's called like what those methods represent, and the struct can have methods and logic itself, allowing for a nice separation of the single parts into individually identified and tested components. Also, Swift encourages this approach by making functions super easy to manipulate and store, so it's a complete win.

The missing 1% here is the fact that in those functions one cannot call super, which is related to point 2., so I'm commenting on it there.

So, for 1. my point is: it's a missing feature, but it's not a huge deal, traditional ways of adding this would be, in my opinion, bad for the language, or imperfect, and there are alternatives that, a the cost of some boilerplate, make for safer and clearer code.

About 2., honestly my participation in this is probably even less relevant than for the first case, because I always felt that APIs designed with the requirement to call super were really bad. super is, to me, truly a glorified goto, that hides important implementation details and makes production code really hard to understand. It's easy to construct toy examples of seemingly useful code that uses super, but in production that code can become a liability. It's really all part of "prefer composition over inheritance" but in the super case the dangers of inheritance are higher because the very code design assumes that something will inherit that implementation and it's supposed to still run that code. There is single case where super makes sense to me, and it's for ensuring the complete initialization of a subclass, but that's just a requirement of inheritance in itself, and would disappear without it.

The problem, to me, lies in the fact that inheritance kills denotational semantics: I can attach semantics to a concrete type (for example if I define a Lazy_ something) and to an abstraction, like a protocol, but when overriding a implementation I can't be sure that I'm not messing around with that type's semantics (for example I could override a method called by some other method that "expects" some particular outcome that I'm going to change without noticing). I'm sure this could be done right enough, but it's dangerous and there are countless alternatives that are safer and clearer: they might require some boilerplate, but that'll pay off in the long run.

Still, without semantics, to understand (or even worse, to debug) a few lines of code we need to follow the various calls around, implementation by implementation, and super greatly exacerbates the whole process. This is, to me, the actual reason to prefer a compositional approach: each piece is isolated and independent, and I'm forced to design the system in order to actually compose the pieces while respecting the semantics. Composition usually comes at the cost of some boilerplate, but that depends on the language, and unfortunately Swift code can be heavy on it, but it's really not a big deal and it pays off in the long run.

So, for 2. my point is: super is up to no good, and there are alternatives that require some boilerplate but make for clearer denotational semantics.

To conclude, given the likely huge effort needed to add an abstract class feature to the language, I can't see equally huge benefits from it, and the language encourages compositional alternatives.

P.S. I write Kotlin from time with my team, and also there we've eventually agreed that avoiding abstract classes in favor of a compositional approach is for the better.

8 Likes

I think the specific solution needs some work, but personally I would be all for a version of abstract classes where the "abstract" properties/methods are defined by a protocol.

That was the intention of that specific post. We don't currently have abstract classes and their addition has been previously deferred. With that in mind, could some of the features discussed for protocols fit the use case for abstract classes.

IMO Swift being a pragmatic language means supporting the design pattern of an abstract Type. The questions is really how it should be supported. Personally I would be hesitant to add too much to protocols, but there may be a middle ground as @Nickolas_Pohilets had mentioned

(Only responding to the above piece of your post.)

Things are different in Obj-C. We're mostly talking (in effect) about abstract base classes. The combination of optional protocol requirements and header files defining interfaces (i.e. public behaviors) separately from their implementation means that the base class type can serve as a stand-in for the common behaviors. If something gets forgotten, the error is detected at run time as a missing method implementation.

In Swift, the base class cannot define an interface (in Swift terms, conform to a protocol) without actually implementing stubs for methods that subclasses are expected to provide.

This means you have to do everything "three times": (1) declare the conformances in a protocol that represents the class hierarchy behaviors; (2) write dummy or default implementations in the base class; (3) write overrides in the subclasses.

That's doable, but annoying. The win here would be to have a way of avoiding boilerplate (#2) so that Swift's methodology is at least as good as Obj-C's.

2 Likes

The only language where I have seen a complete solution for solving name clashes (and as a consequence multiple inheritance) is Eiffel. The basic idea is to rename clashing features if they should be differentiated in the inheriting class (otherwise they fall together). This solves name clashes for properties as nicely as those for functions (no need for interfaces anymore).
An important requirement for that to work properly is that the renamed feature is still found under the original name if called via the parent class, e.g.

class A { 
    var a: Int
}

class B {
     var a: String
}

class Child : 
    A,
    B rename a to b {
}

var y: Child = Child()
y.b

var x: B = y
x.a // will call Child.b
x.b // syntax error: B has no property b

Multiple inheritance - Eiffel Software - The Home of EiffelStudio

https://www.eiffel.org/doc/eiffel/ET-_Inheritance#Multiple_inheritance_and_renaming

2 Likes

I use abstract classes somewhat regularly in library implementations that provide a runtime context for value types that conform to a protocol defined by the library. I'm willing to accept the fatalError pattern in this context as it is part of a library implementation. That said, every time I write it I must admit I miss having the ability to declare the abstract class and methods in a way the language understands.

As others have already pointed out upthread, we need closed protocols, generalized existentials, and the ability for existentials to conform to their defining protocol in order to cover all of the use cases for abstract classes. Both of these features would be necessary to replace the abstract class / inheritance designs I mention abovre with a protocol-based design.

Even then, it may not always be desirable. Sometimes the abstract class is visible to users of the library. Therefore a change to a protocol-based design impacts the API surface of the library. Generalized existential syntax is likely to be somewhat more verbose than generic abstract class syntax (because it will require mentioning the bound associated type names). It's also likely to be perceived as "more complex" by many users (at least until all Swift programmers are very comfortable with generalized existentials).

Despite the use cases for abstract classes I described above I don't have a strong opinion about whether we should add them or not. In the library code I describe above they would be useful. But I certainly wouldn't want to see more traditional OO uses for them becoming pervasive in Swift code. And while I can't speak to the implementation effort required to add them, I can say that in general my preference would also be to see implementation effort channeled into improving protocols and composition. The currently available techniques for implementing "abstraact" classes are good enough, especially when their use is limited to a relatively small amount of library code.

1 Like

Out of curiosity, can you provide an example for this?

What do you mean by "closed"?

Same here, but I reread the thread and couldn't help but noticing that the few examples - of problems that could use an abstract class, not possible solutions - provided in the thread are the result of wanting to follow a specific design approach (let's say "object-oriented" but I disagree with this categorization) that fights against the language instead of taking advantage of what Swift offers.

Still, I 100% agree that Swift generics and protocols have limitations that should be overcome as soon as possible, but from a more theoretical perspective: there are clear "holes" in the generics system, and the already mentioned "improving UI of generics" thread actually tackles most of them. I'd like to see the effort spent there.

Also, I'd like to point out that, while protocols are nice, they are not the solution to everything, and not necessarily to some of the use cases I've seen in the thread. As Ben and Doug shown in Modern Swift API Design, creating instances of concrete generic types instead of defining class/protocol hierarchies is often better, and more theoretically correct in terms of the distinction between "is a" vs "has a". Whenever I see a type that mixes concrete and abstract things, I ask myself why are those together: why not offload the abstract definitions to an instance of another type?

The fact that in Cocoa we need to deal with class hierarchies is a liability, not an asset, same for the necessity to call super.viewDidLoad(): these "ancient" choices in API design should not drive the future of Swift... if anything is the future of Cocoa that should be based on what's currently on the table. Also, most (if not all) of the design patterns shown in GoF (a 25+ years old book) are the product of when and where it was written, and the limitations of the most popular programming languages at the time: the ability to completely reproduce a certain pattern in that book should not be a driver for the future of any language.

I have been using and teaching OO development for some 30 years now.

I wrote an absolutely massive framework (in C#) for a major client, over 7 years, and found abstract classes to be invaluable in many areas.

Frameworks often specify contracts that application developers will inherit from. Abstract classes allow for the definition of a type that, not only, specifies a base "contract" (like a protocol) it also allows for default behaviour and, sometimes, storage that is required, whatever the subclass - the need for storage in an abstract class is especially useful in the case of generic types, where subclasses can explicitly bind the generic type to a special case.

Most often, I use abstract classes to define a base class which it doesn't make sense to instantiate as a standalone class, but which provides both default behaviour and storage that will be required by various specialisations, whose behaviours could not be accommodated in a single class.

My opinion on the direction Swift is taking is that it seems to be trying to avoid OO at all costs, in an attempt to follow a "pure" protocol driven design, whilst "throwing out the baby with the bathwater".

1 Like

Except, Swift supports a number of object oriented features, including classes, inheritance, polymorphism, message passing, etc. You can say what you will about abstract classes, but your broad statement on OO is demonstrably hyperbole at best.

1 Like

A very big hole in Swift's version of OO is the lack of a "protected" scope, which then means we have to either make stuff public or start messing around with managing visibility by trying to fit stuff in modules, simply to maintain, sometimes necessary, information hiding.

Unfortunately, lack of protected scope severely limits what can be done in the way of abstract classes. Maybe for most iOS apps, this might not seem like a big deal but, when it comes to writing frameworks for large and complex macOS apps, it becomes a great deal more important.

Or can you please explain how I can replace an abstract base class, defined in a framework module, which has to be visible in another framework but not in the app that will use that framework, even though the app needs business classes to derive from the second level of derivation.

Don't get me wrong, Swift's accent on protocols and structs is a good thing but, not at the expense of tried, tested and necessary mechanisms for those of us who are working on large, complex apps on macOS.

2 Likes

Sure. But there’s a few levels between zero and a hundred. It’s not all or nothing. That Swift’s OO is lacking some features isn’t the same as saying Swift is trying to “avoid OO at all costs” when that is clearly false.

2 Likes

I notice a distinct lack of examples of how to replace abstract claases and protected scope. Surely someone who is against such constructs could be good enough to suggest a replacement?