Equivalent of class methods, but for protocols (Factory Methods / Class Clusters)

Problem Statement

One of the patterns I really like in Swift is the ability to use the dot shorthand when working with static functions. For example, if I have this code:

extension UIFont {
  static func helvetica(size: CGFloat): UIFont { return UIFont(name: "Helvetica", size: size) }
}

I can then use that without the UIFont prefix, which makes the code really nice:

label.font = .helvetica(size: 20)

Unfortunately, if that were a protocol, I'd have an unfortunate side effect of that static function being able to be called directly on any implementation of the protocol. So if I did this:

protocol Image {
  func uiImage() -> UIImage
}

extension UIImage: Image {
   func uiImage() -> UIImage { return self }
}

extension Vector: Image {
   func uiImage() -> UIImage { return functionToConvertToUIImage() }
}

and wanted to have my images be accessible as Image.backArrow, Image.appIcon:

extension Image {
  static var backArrow: Image { return Vector(name: "backArrow") }
  static var appIcon: Image { return UIImage(name: "appIcon") }
}

Unfortunately, because of the way protocols work, that also means I would be allowing this, which breaks expectations for me:

UIImage.backArrow
Vector.appIcon

"backArrow" is not a UIImage and "appIcon" is not a Vector. In short, I often find myself wanting to make a factory off of the protocol, but am unable to limit methods or functions to the protocol itself, which I think would be useful.

Proposal

Following the pattern of class methods, in which you can do this:

class Image {
  class var backArrow: Image { return Image(named: "backArrow") }
}

it would look like this:

protocol Image {
  func uiImage() -> UIImage
}

extension Image {
  protocol var backArrow: Image { return Vector(named: "backArrow") }
}

The "protocol" in front of the var would mean that backArrow would only be available on the Image protocol directly, but not on any object that implements that protocol.

I would like to allow initializers, functions and variables to be used this way:

extension Image {
  protocol init(isBlue: Bool) { return isBlue ? BlueImage() : RedImage() }
  protocol var backArrow: Image { return Vector(named: "backArrow") }
  protocol func backArrow(isInverted: Bool) { 
    let a = UIImage()
    if isInverted { a.invert() }
    return a
  }
}

let a = Image(isBlue: true)
let b = Image.backArrow
let c = Image.backArrow(isInverted: false)

This would allow similar behavior to things like class clusters and factories that Obj-C supported.

The previous threads I've seen in Swift evolution about factories have been with regard to initialization on classes, but I feel that having them on protocols fits better with the protocol oriented programming model that Swift pushes. Is this something that the core team has thought about?

1 Like

My concern here is that we would be tying a protocol to an concrete implementation. Eg your Image protocol would be intrinsically tied to UIImage and Vector. Is this what we want, or are we promoting anti patterns?

1 Like

Can you elaborate on that? I'm not sure how it's more tied together than an extension with a default implementation for a static function or var, which you can already do.

The above self return the concrete type not the protocol metatype.


protocol Feline {
    static func purr() -> Feline
}

struct Cat {}
extension Cat: Feline {}

extension Feline { // These methods do not get attached to the feline metatype and can only be called via their concrete types.
    static func purr() -> Feline {
        return Cat()
    }
    
    static func meow() -> Cat {
        return Cat()
    }
}

let concreteCat:Cat = .meow() // Okay
let felineMeta:Feline = Cat.purr() // Okay


let someCat:Cat = Feline.meow() // error: static member 'meow' cannot be used on protocol metatype 'Feline.Protocol'
let someFeline:Feline = .purr() // error: static member 'purr' cannot be used on protocol metatype 'Feline.Protocol'

You can just put the known instances somewhere else, which tends to be a better design in most of the real examples I've seen:

protocol Image { ... }

// caseless enum, for namespacing purposes only.
// 'KnownImages', 'StandardImages' are also good names.
enum NavigationImages {

  static let backArrow: Image = UIImage(named: "backArrow")

  static func cancelButton(foreground: UIColor = .black, background: UIColor = .clear) -> Image { 
    return Vector(...)
  }
}

myPanel.closeButton = NavigationImages.cancelButton(foreground: .red)
1 Like

I see your point that we can already tie your definition to require the import of the UIImage type by adding an extension that requires every Image to be convertible to a UIImage, for example. This ties the protocol to require that type by definition. Good point.

My only other concern is... does it make sense to ask the abstract concept of an image for a backArrow, and get a specific concrete type back? I’m not sure either way to be honest.

In theory the requested behaviour can be satisfied once existentials extensions would be allowed (which is not confirmed it will happen). e.g.

extension any Image {
  static var backArrow: some Image { return Vector(name: "backArrow") }
  static var appIcon: some Image { return UIImage(name: "appIcon") }
}

This does couple Image with concrete implementations, but it's fine as long as it's all done by the code that defines the protocol in the first place. Sequence is full of such examples :slight_smile:

That breaks the shorthand in my example without providing extra benefit.

For the core team, there's been a lot of discussion on [Proposal] Factory Initializers around this, though it was factory methods on classes, not protocols. it seemed to have a lot of interest but then got swept up in ABI stability. It seemed to have a lot of interest at the time.

I have wanted something like this for a while, and I think the functional argument in favor of it is quite strong.

The absence of this feature can easily stymie attempts to convert object-type-based APIs to the more flexible world of protocols. For example, if you have the following API:

func countOnQueue(_ queue: DispatchQueue) { ... }

Clients will naturally tend to use dot notation when calling it, e.g.:

countOnQueue(.main)

This isn't just a sugary frippery; it's one of the nicer affordances created by Swift's type inference.

Unfortunately, there is no way to redefine this API in terms of a protocol without breaking existing code:

protocol Dispatcher { ... }
func countOnQueue(_ queue: Dispatcher) { ... }
...
countOnQueue(.main)  // Bzzzzt!

You can kind of get around this by adding wrappers, but it's a maintenance nightmare.