Abstract class in Swift?

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

Here is an example of a class I use in my library:

func abstractMethod() -> Never {
    fatalError("need to implement this in subclass")
}

open class Domain {
    
    fileprivate let _rep : Rep
    
    public required init(rep : Any) {
        if let r = rep as? Rep {
            self._rep = r
        } else {
            fatalError("invalid Domain rep: \(rep)")
        }
    }
    
    public required init() {
        self._rep = .None
    }
    
    public required init(domainValue : Any) {
        abstractMethod()
    }
    
    func rep() -> Rep {
        precondition(!_rep.isNone)
        return _rep
    }
    
    func domainRep() -> DomainRep {
        abstractMethod()
    }

    public final func from(domainValue : Any) -> Self {
        return Self(domainValue: domainValue)
    }
    
    public func encode(domainValue : Any) -> DomainValueEncoding {
        abstractMethod()
    }

    public func decode(domainValueEncoding : DomainValueEncoding) -> Any {
        abstractMethod()
    }

}

You are telling me this dynamically checked perversion is better than just writing:

open class Domain {
    
    fileprivate let _rep : Rep
    
    public required init(rep : Any) {
        if let r = rep as? Rep {
            self._rep = r
        } else {
            fatalError("invalid Domain rep: \(rep)")
        }
    }
    
    public required init() {
        self._rep = .None
    }
    
    public abstract init(domainValue : Any) 
    
    func rep() -> Rep {
        precondition(!_rep.isNone)
        return _rep
    }
    
    abstract func domainRep() -> DomainRep 

    public final func from(domainValue : Any) -> Self {
        return Self(domainValue: domainValue)
    }
    
    public abstract func encode(domainValue : Any) -> DomainValueEncoding 

    public abstract func decode(domainValueEncoding : DomainValueEncoding) -> Any 

}

If you do, we have very different opinions about software design :-)

3 Likes

Actually, I think the above suggests an easy implementation of abstract classes: Just introduce an abstract qualifier as shown above, and say that the semantics is the same as if the body would have been implemented via fatalError. That is an easy rewriting step. Then go ahead and support abstract in the type system via warnings / errors at compile time.

1 Like

No, you misunderstand me. If you find an abstract class in your codebase there may be other problems to solve that use more functional rather than object-oriented solutions. It's rarely a case of interface design, the trees, and rather symptomatic of your holistic architecture, the forest.

1 Like

Yeah, that's just religion.

So just to iterate on that, there would be no such thing as an abstract class, but just abstract initialisers / methods. That should help with dealing with corner cases I've seen people talk about in the evolution mailing list. It is very similar to what @GetSwifty mentioned earlier.

I would also tend towards finding a more functional/POP solution but Swift, as a language, isn't dogmatic about specific design patterns. OOP has a long history and a massive user/knowledge base. Swift supports users who prefer that approach as well as users who prefer a functional approach. Not to mention it's current core usage is in one of the largest OOP-library ecosystems around.

4 Likes

One of the things that I was hoping to see is an extension of Compiler Diagnostics (SE-1096) within a particular compile path.

What I mean is more akin to the constexpr if type of thing in C++, where if we have:

In the example above with abstractMethod invoked in the base implementation that simply calls fatalError, instead you would just put an #error(...) in the base implementation.

During compilation, if the compiler detects that the base implementation is invoked, it will trigger the compile error.

1 Like

Right, Swift specifically supports OOP use cases like abstract classes with its fatalError function.

Yeah, that's just religion.

Judaism, Christianity, and Islam are religion. This is dialectic. Dismiss it at your peril.