[Random Thought] Optional Protocol Methods + Mixins


(Jon Hull) #1

Hi Everyone,

I have been mulling over the problems of optional protocol methods and (separately) the mixin / diamond problem over in my head for a week or so now, and I wonder if they might have a common solution.

The main philosophical dispute seems to be around what we call the "system image” in User Experience. The folks from the C++ & Java world are most comfortable when all of the levers and gears are openly exposed because it allows them to better reason about and predict the system’s behavior. Those from the Smalltalk/Python/ObjC world want progressive disclosure, where they don’t really have to even think about the minutiae unless it becomes important, so they can focus on their solution instead of abstract mathematics. When it becomes important, then they want to be able to access all of those levers, but before that, they should be hidden away and everything should "just work”. (This is why ObjC had only 3 main collections hiding implementations behind class clusters and Java has things like “ConcurrentSkipListMap”)

Swift has been charting the waters between these two worlds, trying to provide for both where possible, and I nothing I say today will unify those philosophical differences. I might however, have a swift-ier way to provide the functionality/flexibility that optional methods gave us in ObjC without actual optional methods.

The ability to provide default protocol implementations gives us most of the benefits of Optional methods on the protocol provider’s side. If there is a default implementation, you only need to provide an override if you need special behavior. There are suddenly a bunch of functions you don’t have to worry about unless you need them… progressive disclosure. ObjC folks are relatively happy (and it still allows a good deal of compiler optimization to boot).

The one issue is on the caller’s side we no longer have information about whether the protocol implementor overrode the default or not. In ObjC, it was common to use the information of whether an implementation was provided to optimize things behind the scenes. We could provide a slow implementation for the general case, but then provide much faster implementations when we had the guarantee that a certain option/feature would not be used. Several people have suggested adding an explicit extra method where the programmer can signal their intent, but this now allows for new errors. Before we had a compiler/runtime guarantee that a feature wasn’t used because we know the code to use it doesn’t exist. The explicit extra method can now get out of sync with the feature implementation (the programmer might forget to signal or they might forget to remove the signal after refactoring). There is a much larger API to learn, and there is more to go wrong…

That said… I think that we could remove optional methods (using Douglas’ backwards compatible proposal for ObjC code) and replace them with default implementations (with the understanding that we will get our optimization capabilities back in a moment using a different mechanism). I think everyone also agrees that we need to mark the generated interfaces of methods which do not have default implementations as “required” (or something similar).

Ok, so now we always know we can call a method in the protocol and we know it will have an implementation… no need to check if it is there first. The language just became a little bit simpler and more consistent.

Now let’s turn to another issue which has come up from time to time. It would be nice, when overriding a protocol's default implementation, to be able to call the default implementation (kind of like calling super in a class). Similarly, when we end up providing mixins, we will need a way to disambiguate and select between the implementations. I have also occasionally run into a case or two where I want to be able to call a specific ancestor of a class (e.g. super’s super) and get that implementation (I used to use a language where that was easy, and I always missed it in ObjC).

In short, we end up with several different cases where we want to be able to be able to define which implementation we are calling among several options. In classes, we use super, but with mixins and protocols, it can be much more complex. We really just want to be able to explicitly state which implementation to use. There are lots of syntax options being thrown about (e.g.
'implements P’). I will use the throwaway syntax of ‘instance.P::method’ just so we have something concrete to talk about (I don’t expect that to be the actual syntax). That statement means that ‘method’ is called on ‘instance’ using the implementation in ‘P’. Again, there is better syntax, but it will need those three pieces of information.

Now for the punchline.

We know that we can always call a method on a protocol because we know there is an implementation for it somewhere. Different methods may have been provided in various places (and some in several places), but together they cover the contract. Once we start referencing specific implementors though, that guarantee doesn’t always hold (especially if we allow P to be generic). The compiler still knows who implemented what of course, but we can’t just assume that every implementor of a protocol has overridden a particular default, and we shouldn’t fall back to the default when we are asking for a particular implementation.

The answer, of course, is to treat those cases exactly how we treat optional methods today. We can just use optional chaining, etc… and the compiler will force us to deal with the cases where we can’t guarantee that the method exists. We have gained back the missing capabilities of optional methods with the important difference that we don’t need to tag methods as “optional” and we are guaranteed that we can always call ‘instance.method’ safely. It is only in the cases where we are asking for a particular implementation that we have to worry about the added complexity. Progressive disclosure.

At the same time, we have also gained the ability to reference a protocol’s default implementation from an override and to disambiguate between competing protocol implementations.

Thoughts? A direction worth exploring further?

Thanks,
Jon


(L Mihalkovic) #2

I was reading the document and was wondering if it might make sense to look into using definitions rather than conventions:

public protocol LiteralCreatable {
    associatedtype LiteralType
    init(literalValue : LiteralType)
}
public protocol NilLiteralConvertible : LiteralCreatable {
    associatedtype LiteralType = ()
}
public protocol StringLiteralCreatable : LiteralCreatable {
    associatedtype LiteralType = String
}
public protocol IntLiteralCreatable : LiteralCreatable {
    associatedtype LiteralType = Int
}

which leads to the following code:

struct TT { }
extension TT: IntLiteralCreatable {
    init(literalValue: Int) { }
}
extension TT: StringLiteralCreatable {
    init(literalValue: String) { }
}
let tt1 = TT()
let tt2 = TT(literalValue: 20)
let tt3 = TT(literalValue: "StringLiteral")

or even this:

struct XX { }
extension XX: IntLiteralCreatable, StringLiteralCreatable {
    init(literalValue: Int) { }
    init(literalValue: String) { }
}
let xx1 = XX()
let xx2 = XX(literalValue: 20)
let xx3 = XX(literalValue: "StringLiteral")

cheers
LM/