Abstract class in Swift?

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.

9 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.

6 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.

1 Like

I like protocol-oriented programming, and it works well. I wouldn't add any fancy multiple inheritance, stored properties in protocols stuff etc., but it seems to me that abstract classes are much simpler than that. I just want to avoid having to put fatalErrors in superclass method stubs, instead I want these fatalErrors go away in favour of compile-time checks. That seems simple, and pragmatic, and not too difficult to implement, as it doesn't really add any functionality, but rather just forbids some behaviour. Is there any chance we will get this?

2 Likes

I can’t remember if I ever actually pitched it, but at one point I was seriously wanting a mechanism; similar to fatalError, for expressing something like “Yes, ok, I know I have to put this function/computed property/whatever here to make the type checker happy, but I want to make it a compile-time error to actually link to or use it because I’m trying to do something the compiler doesn’t understand.”

(I no longer remember exactly what I was doing, but knowing me it probably had something to do with figuring out a way around Swift not supporting integer literals as generic types.)

One option rather than a typical "Abstract Class" would be to add some modifiers for Methods similar to private(set), e.g. (throwing out ideas here) required(implement) where a subtype is required to implement the method, and required(call) to say a subtype has to call super's implementation.

Another (roundabout) way would be a way to trigger a compile-time warning/error if a method is used so that a faux-Abstract Class could at least give the needed warnings.

More generally I'm unsure how many new OOP-specific features are likely to land in Swift. Protocols and Generics can be made to accomplish many of the same design patterns, and IME complex Class structures are rare to see in Swift outside of legacy-libraries, (e.g. UIKit). Some go as far as saying it's an anti-pattern, which seems like a step too far, but the sentiment/trend is there regardless.

1 Like

Not really a fan of required, but that would be a possibility.

I just think that if you allow open classes, then you are admitting that these classes will have some functionality provided by subclasses. From there it immediately follows that abstract classes are needed. If you can say open, you must also be able to say abstract. Anything else is an omission from the language, which one can obviously live with, but why?

2 Likes

Yes, but I would argue that if a third party framework requires subclassing an open class and it isn't a legacy interface from Objective-C, then its a "code smell" in that framework.

Even with Objective-C, we have delegates and data sources to attempt to reduce the amount of subclassing you need to do to change behavior.

The only instance of an open class in the stdlib is the Encoders/Decoders, which IMHO is a mistake as the internals needed to change encoding/decoding behavior are not externally accessible. I also feel that in this case it is a sign of a bad API - the reason that Encoders have to be classes is because the design of the container interface behind Encodable mandates containers have reference semantics.

1 Like

I am currently implementing a Parsing DSL where using open classes works very well, and I rather like the smell of it. It is not published yet, but I can already give an example:

class Calculator : Grammar<CODEPOINT> {
    
    @Nonterminal var Expr : Out<INT>
    @Nonterminal var Sum : Out<INT>
    @Nonterminal var Product : Out<INT>
    @Nonterminal var Num : Out<INT>
    @Terminal var Digit : Out<INT>
    
    var ambiguous : Bool
    
    init?(ambiguous : Bool) {
        self.ambiguous = ambiguous
        super.init()
    }
    
    override func build() {
        add {
            start($Expr)
            
            $Expr.rule {
                $Sum
                
                $Sum.out --> $Expr
            }

            $Sum.rule {
                $Sum[1]
                %!(char == "+")
                $Product
                
                $Sum[1].out + $Product.out --> $Sum
            }

            $Sum.rule {
                $Product
                
                $Product.out --> $Sum
            }

            $Product.rule {
                $Product[1]
                %!(char == "*")
                $Num
                
                $Product[1].out * $Num.out --> $Product
            }
            
            $Product.rule {
                $Num
                
                $Num.out --> $Product
            }
            
            $Num.rule {
                $Digit
                
                $Digit.out --> $Num
            }
            
            
            if !ambiguous {
                $Num.rule {
                    $Num[1]
                    $Digit
                    
                    $Num[1].out * 10 + $Digit.out --> $Num
                }
            }
            
            if ambiguous {
                $Num.rule {
                    $Num[1]
                    $Num[2]
                    
                    $Num[1].out * 10 + $Num[2].out --> $Num
                }
            }
            
            $Digit.rule {
                %!(char.codepoint >= 48 && char.codepoint <= 57)
                char(0).codepoint - 48 --> $Digit
            }
        }
    }
    
}

You create a grammar in my DSL by subclassing from Grammar which is an open class. This is very natural, and I don't see anything smelly about that. It would not be possible to use a protocol instead, as Grammar keeps track of internal state that the user of the library should not be exposed to.

3 Likes

Oh, that looks neat! Do you know if/when it'll be made public?

Glad you like the look of it :-) It'll be public before the end of the year.

1 Like

Or...make it a struct and pass in the builder :slight_smile:

let calculator = Grammar<MyCodePoint>(ambiguous: true, builder:  { (grammar: Grammar< MyCodePoint) in 
        grammar.add {
            grammar.start(grammer.$Expr)
      
            grammar.$Expr.rule { 
            // etc
})

Not saying you should, (obviously I haven't seem the whole structure so I can hardly make that judgement anyway), just that there are usually options.

Anyway, other than the weird casing :wink: it looks cool!

1 Like

That looks like I have to repeat grammar. a thousand times there, just in order to adhere to a dogma ... ;-) Doesn't look like an improvement. But playing the devil's advocate here, could I get rid of the grammar repetition in your example somehow, via something like import grammar._ ?

It's as @GetSwifty says, abstract classes are considered an anti-pattern. fatalError already allows developers to declare them anyways.

I'd say fatalError is an anti-pattern here. Abstract classes are not an anti-pattern if properly used, just like continue and break are not an anti-pattern if properly used. If you don't know when to use a tool, that's a different issue entirely.

1 Like

At least we can agree on one thing :laughing:

1 Like

I think the most you can do is use a short name (or just $0). If we could do something like kotlin's apply it would allow using it, but I'm doubtful we'll see something like that in Swift anytime soon.

I don't think I went that far...regardless there are a lot of (IMO) valid use cases for that type of structure in an OOP ecosystem. Swift having structures that enable other solutions doesn't invalidate all use cases :slight_smile:

1 Like
Terms of Service

Privacy Policy

Cookie Policy