Default arguments for Protocol methods in Swift

Swift Protocol Extension Jan 22, 2021 · 4 min read

I am going to give a simplified example from the Templating component of Chaqmoq (A server-side web framework in Swift) that depends on Yaproq (A templating language and engine for Swift). For example, a common interface for all templating engine implementations might look like this. The renderTemplate method has two parameters named path and context. The path refers to a place where a template file is located in the FileSystem of a particular OS while the context contains a list of variables in a template file to process.

public protocol Templating {
    func renderTemplate(at path: String, in context: [String: Encodable]) throws -> String
}

Now, we can create a concrete class named ConcreteTemplating as one of the templating engine implementations. When we provide a list of variables or an empty dictionary in a form of .init() or [:] when calling the renderTemplate method, it works like a charm. But, when we omit the context argument, it shows an error because there is no default value for the context parameter in the definition of the method. Well, developers who use our library don't always need to provide the context argument because they can also have static templates without any placeholders for variables.

public protocol Templating {
    func renderTemplate(at path: String, in context: [String: Encodable]) throws -> String
}

final class ConcreteTemplating: Templating {
    func renderTemplate(at path: String, in context: [String: Encodable]) throws -> String {
        return "Hello World" // Returning a static string for simplicity
    }
}

var templating = ConcreteTemplating()
try templating.renderTemplate(at: "/templates/index.html", in: ["title": "Home"]) // It works
try templating.renderTemplate(at: "/templates/index.html", in: .init()) // It works
try templating.renderTemplate(at: "/templates/index.html", in: [:]) // It works
try templating.renderTemplate(at: "/templates/index.html") // Error: Missing argument for parameter 'in' in call

A simple solution can be adding a default value with .init() or [:] to the context parameter of the renderTemplate method in the ConcreteTemplating class.

public protocol Templating {
    func renderTemplate(at path: String, in context: [String: Encodable]) throws -> String
}

final class ConcreteTemplating: Templating {
    func renderTemplate(at path: String, in context: [String: Encodable] = .init()) throws -> String {
        return "Hello World"
    }
}

var templating = ConcreteTemplating()
try templating.renderTemplate(at: "/templates/index.html", in: ["title": "Home"]) // It works
try templating.renderTemplate(at: "/templates/index.html") // It works

But, what if our app logic requires to work with multiple templating engine implementations and be able to change them at run-time in a polymorphic way? To do that, we have to explicitly set the type of var templating to the Templating protocol type. When we do that, the renderTemplate method called without the context argument stops working because the compiler refers to the method in the Templating protocol instead of the ConcreteTemplating class.

public protocol Templating {
    func renderTemplate(at path: String, in context: [String: Encodable]) throws -> String
}

final class ConcreteTemplating: Templating {
    func renderTemplate(at path: String, in context: [String: Encodable] = .init()) throws -> String {
        return "Hello World"
    }
}

var templating: Templating = ConcreteTemplating()
try templating.renderTemplate(at: "/templates/index.html", in: ["title": "Home"]) // It works
try templating.renderTemplate(at: "/templates/index.html") // Error: Missing argument for parameter 'in' in call

We might say that is easy because we can move that .init() from the ConcreteTemplating class to the Templating protocol and the problem is solved. Well, no. Swift doesn't support default arguments for Protocol methods.

public protocol Templating {
    func renderTemplate(at path: String, in context: [String: Encodable] = .init()) throws -> String // Error: Default argument not permitted in a protocol method
}

final class ConcreteTemplating: Templating {
    func renderTemplate(at path: String, in context: [String: Encodable]) throws -> String {
        return "Hello World"
    }
}

var templating: Templating = ConcreteTemplating()
try templating.renderTemplate(at: "/templates/index.html", in: ["title": "Home"]) // Error: Value of type 'Templating' has no member 'renderTemplate'
try templating.renderTemplate(at: "/templates/index.html") // Error: Value of type 'Templating' has no member 'renderTemplate'

But, we can achieve what we want using a simple trick with extension. We can create the same method without the context parameter whose implementation doesn't do anything other than calling the renderTemplate method in the Templating protocol with .init() as a default value for the context parameter.

public protocol Templating {
    func renderTemplate(at path: String, in context: [String: Encodable]) throws -> String
}

extension Templating {
    public func renderTemplate(at path: String) throws -> String {
        try renderTemplate(at: path, in: .init())
    }
}

final class ConcreteTemplating: Templating {
    func renderTemplate(at path: String, in context: [String: Encodable]) throws -> String {
        return "Hello World"
    }
}

var templating: Templating = ConcreteTemplating()
try templating.renderTemplate(at: "/templates/index.html", in: ["title": "Home"]) // It works
try templating.renderTemplate(at: "/templates/index.html") // It works

Conclusion

Even though Swift doesn't support default arguments for Protocol methods, you can achieve the same result by using this simple trick. But, I want to point out that if your method has more than one parameter with a default value, the number of methods you must create in the extension of the Templating protocol will increase and this trick only works for parameters with concrete types.