Native Optional Methods in Swift Protocols

Speaking for myself, I don't think things must remain as they are merely because it was decided so in the past: I fundamentally think it was the right decision. While optional requirements fit in with the overall dynamism of Objective-C, I don't think it fits well with Swift. I think in terms of user experience for conformers, requirements-with-default-implementations is virtually identical: you declare a conformance, and then see what requirements the compiler makes you implement.

There is, perhaps, a slight degradation of experience for folks who operate by first pulling up the protocol definition and looking at all the requirements, copying them over, and trying to implement each one. So from that standpoint I see the value in having something attached to the requirement declaration. But I pretty strongly feel that the pattern of 'condition the protocol client behavior on whether or not the conformer implemented a particular method' is confusing unless there's an 'obvious' no-op behavior, in which case it should be easy to provide a default implementation.

I'm kind of meandering here but I suppose that the upshot is that I wouldn't support a simple 'have first-class optional requirement support for protocol requirements without @objc' proposal, but I can at least see an arguable case for a version of this proposal where optional on a non-@objc requirement means 'protocol author promises to provide an unconstrained default implementation for this method, and it's an error if no such default can be found'. Of course, this would make optional amount to little more than glorified documentation which protocol authors could choose to apply or not, and so I'm not sure the juice is worth the squeeze at that point compared to the status quo: expecting protocol authors to note the optionality (and default behavior) of a protocol requirement in the first place!

12 Likes

The way the standard library currently implements optional requirements is by making the return value of method optional and adding a default implementation that just returns nil e.g.:

The later gets a double optional result value.

This works great if the arguments are readily available and do not need to be conditionally constructed, depending on if the requirement is customized or not.

Any concrete use case where this strategy would be a problem?

3 Likes

I must first express my sincere appreciation for the many thoughtful proposals and the historical context that have been so carefully presented. It is both humbling and inspiring to be engaged in this conversation within such a principled and intellectually rigorous environment.

That said, if we are to remain faithful to Swift’s core philosophy—the ability to express intent clearly, concisely, and safely—I would respectfully suggest that the current workaround approaches, while technically valid, verge on being unnecessarily verbose.

In contrast, consider the following syntax:

protocol TextFieldDelegate {
    func textFieldDidEndEditing(_ textField: String)
    optional func opt(at model: OriginalModel)
}

Here, the distinction between required and optional requirements is immediately evident. The intention is not buried in protocol extensions or scattered across conformance logic. It is declared clearly and elegantly, at the site of definition. This aligns not only with the Swift ethos of clarity and safety, but also with the aesthetic principle that code should, where possible, be self-documenting.

Indeed, is this not a more beautiful form of expression?

I would thus kindly ask the community to reconsider whether a return to this simplicity may, in fact, better serve the language and its users than continued reliance on increasingly elaborate alternatives.

With respect and collegiality,
keisukeYamagishi

1 Like

This pattern also has the (IMO desirable) property that it is trivial for any conformer to explicitly adopt the default behavior. Sometimes this might just be for clarity/future-proofing (e.g., I want to “always” get the no-op behavior even if the default implementation), but sometimes it might actually be necessary, e.g., when a superclass has implemented non-default behavior and a subclass wants to restore it. When you’re in Objective-C land you at least have the nuclear option of overriding -respondsToSelector: if you really need to ‘restore default behavior’ but in Swift land you’d be out of options if we naively ported optional requirements without some additional supporting feature.

(It would also be nice if there were an explicit syntax for calling the default implementation provided by a protocol author rather than having to reimplement it yourself.)

3 Likes

As I mentioned above, I definitely see the value in being able to look at a protocol definition and easily tell which requirements a conformer must implement. In my mind the main thing to avoid with any reintroduction of this feature to non-@objc protocols would be to prevent the ability to do an 'implements-test' against the requirement. I.e., it should not be supported for a client of the protocol to ask 'does this conformer implement optional requirement X?'. Another alternative design would be to say that optional can only be applied to Void-returning or Optional-returning requirements and the trivial default implementation would be automatically synthesized.

This also seems like something that macros could or ought to be able to help with! If we had an @Defaulted macro today which tried to do what I described above (synthesize a default implementation for Void- and Optional- returning methods), how far would that get us? What features would be missing to make that macro function properly?

4 Likes

see the value in being able to look at a protocol definition and easily tell which requirements a conformer must implement

I would go further to believe that this is the primary purpose dependency with protocol. Per default implementations, I've definitely been bit before due to an unknown default implementation - I would almost be glad to see them gone (but don't feel strongly about it).


The comparison to init? was made and I think is incorrect. This init defines contextually that "the type cannot be constructed given the arguments", always due to some kind of internal logic (ie: invalid String value). Using Optional as a return/general type value brings specific semantics, mostly that some value doesn't exist due to some other defined logic. It is typically always a bug if specific logic isn't implemented in the first place. While > 1 Optional values may also be a bug in some contexts, some will say they have their purpose.

Thinking about the usage of an optional function, the language today wouldn't make this usage pleasant since we would have to first "unwrap the function" and lose argument labels.

protocol Bar {
    optional bar(x: Int, y: Int, z: Int) -> String?
}

class Foo: Bar {}

let a = Foo()
// bar: (Int, Int, Int) -> String?
guard let bar = a.bar else { ... }

cognitive overhead

This would introduce all the cognitive overhead, now we have to actively check whether specific types actually implemented some optional function. The only way to check for this would be the unwrapping above and printing or somewhat. A single debugging session within a generic function using an optional function would be enough to dissuade almost all usage, imo.

I can't see FooEx inheriting from Foo brings any benefit. So I suspect you meant the following?

protocol Foo {
    var value: Int { get set }
    func a()
    var asEx: (any FooEx)? { get }
}

protocol FooEx {
    static func b(value: Int)
    static func c(value: Int)
}

This is similar to your first approach. IMO both approaches have the same limitation - you have to pass value to function b and c explicitly, because it's inaccessible to them. After reading through all workarounds in the threads, my conclusion is I'd go with the suggestions in SE-070 (using default implementation or protocol inheritance).

This topic kept sucking me back into a thought process ever since I came across it. It took me a while to articulate why I wouldn't want an optional func but some alternatives I could see as valuable.

First of all, I understand the desire for an optional func based on two arguments:

  1. Optional requirements are useful to expand a preexisting set of constraints without breaking API. By explicitly and clearly providing an alternative. This is kind-of covered by default implementations already.
  2. But more related; they provide an explicit default definition that can be observed both in code and documentation. No need to search through all extensions on that protocol.

Protocols are a great abstraction tool, but I like to think of them practically only in the context of generics. Back in the early days of Swift, I've come across projects where there were about as many protocols as concrete types. This complicates the implementation of a project so much that all of those projects have undergone rewrites since.

Just like over abstraction (too many protocols) can be hard to wield, optionals are pretty similar. Over use of optionals make it very hard to write correct code, as you don't have any guarantees! Taking that to the extreme is making everything an Any - you get zero guarantees.

Optional functions would require checking if the method exists before calling it, and falling back to a different implementation otherwise. The benefit of that could be a default implementation, but other than that it's unclear to me what (besides headaches) that might offer.

Besides that, I read a couple authors referring to optional func as an optional requirement. This coincides with the language handbook which states:

Protocols: Define requirements that conforming types must implement.
A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality....

Must implement is a clear starter here, which is very much a part of the Swift mental model. In addition, a requirement to me specifies it must be present. I can't require something that is a prerequisite and then accept it not being present for a given task.

Also, for consistency I would argue that an optional func implies the existence of other "optional requirements". So would we also add optional var myProperty: Int { get }?
What about optional associatedtype?

However, I think default implementations are a valid thing. And I have to agree with the argument that clearly defining the precense of a default implementation in a protocol's makes sense.

What I could find a logical conclusion is the following future direction. We'd allow implementing a function body (or computed property body) as part of the protocol definition.

protocol MyDelegate {
  func didReceiveEvent(_ event: Event) {
    // Ignore or handle the event as a default behavior
  }
}

Besides increased clarity on the availability of a default, I think this would also help library authors expand existing protocol requirements without breaking API.

3 Likes

Could Xcode show it somehow? e.g. by using a different style for functions that are backed by default implementation, or by overlaying "optional" on top and to the right? Will be Xcode (or other IDE) only feature, not visible in terminal or on web, etc, but that might be good enough.

This one is easy to solve, we can just call it differently :slight_smile:
e.g. "optional protocol members".

Yes, sure.

Definitely not.

I kind of like this one. The slight worry is people starting writing war & piece size functions in there.

In my view the bigger risk is that it makes the difference between:

protocol P {
  func f() { ... }
}

and

protocol P {}
extension P {
  func f() { ... }
}

extremely subtle and I expect users would accidentally end up introducing customization points in the main protocol body when they really just meant them to be extension methods. One way to split the difference would be to still require the body-less declaration, but permit default implementations in the main body as:

protocol P {
  func f()
  func f() { ... }
}
4 Likes

How about

protocol P {
  func f() @defaultImplementation {}
}

of

protocol P {
  optional func f() {}
}

Inheritance is not a mandatory part of the pattern, but it is handy if FooEx and Foo are always used together. Then it makes sense to combine them into a single entity using inheritance.

1 Like

Inconsistency between protocols' and types' extensions always bothered me. Logically it should have been:

protocol P {
    func foo()
    func bar()
    ....
}
// The above was quite long, so let's continue elsewhere.
// Or we do this for other reasons,
// e.g. we could continue in a different file
// like with types

// elsewhere
extension P {
    func baz()
    func qux()
    ...
}

// possibly even:
extension P: SomeOtherProtocol {
    ...
}

i.e. align how we use extensions for types and protocols...

And for what we currently use "protocol extensions" we'd use a different keyword:

// bike-shed name follows:
implementation P {
    func foo() {
        // default implementation
    }
}
1 Like

IMO, the current treatment is more consistent insofar as the main declaration body of protocols and types are both expected to contain the 'core' of the type and extensions only provide auxiliary functionality. E.g., a type's storage must be confined to the main type declaration just as the requirements of a protocol must be confined to the main protocol declaration.

6 Likes

This is not quite supported in concrete types either. You can’t add stored properties this way for example.

1 Like

Quite often than not I find default implementations on protocols to backfire and complicate things, so tend to avoid if possible. There are valid cases for default implementations, of course. But this capabilities of the language imho cover all possible cases as optional methods do if you need it, without introducing more optionality or dynamic complexity.

2 Likes

I concur, I don’t think this should actually be expanded into a real change in Swift. But I kind-of understand the arguments for clarity, though not actually optional functions.