First parameters, Swift signatures, and conditional builds

One of the many challenges in moving code from Swift 2.2 to Swift 3 is dealing with changed method signatures. For example, say you have the following function in Swift 2.2:

func frobnicate(runcible: String) { 
    print("Frobnicate: \(runcible)") 
}

You call this with frobnicate(string) in 2.2 and frobnicate(runcible: string) in 3. The new first label rule introduced in SE-0046 means that  you have to differentiate calls based on how they consume this already existing function:

// Consuming
func frotz() {
    #if swift(>=3.0)
        // Swift 3.x code
        frobnicate(runcible:"frotz 3.x " + string)
    #else
        // Swift 2.2 code
        frobnicate("frotz 2.2 " + string)
    #endif
}

Remember that Swift’s compile-time build directives must surround entire statements and expressions. You cannot “cheat” to surround only the runcible: label. (And even if you could, it would look horrible.) The expression limitation means you’ll need to take care when introducing multi-version code.

The previous example worked by differentiating code at the call site. An alternative approach involves leaving the consumer untouched and supplying consistent API signatures based on the Swift distribution used to compile the code. That approach looks like this:

// Supplying consistent API signatures
#if swift(>=3.0)
    func fripple(_ fweep: String) {
        // Full 3.x codebase
        print("3.x fripple", fweep)
    }
#else
    func fripple(fweep: String) {
        // Full 2.2 codebase
        print("2.2 fripple", fweep)
    }
#endif

In this example, both fripple implementations can be called without first labels.

The big problem here is that you must duplicate the full codebase for both implementations, even if they are essentially or entirely the same. If you fix a bug in one implementation, you must mirror that fix in the other implementation.

Fortunately there is a way around this. It works by unifying shared code in a separate closure. By moving the code out from the functions or methods, you can treat the signatures and the implementations as separate configurations and avoid code duplication.

// Alternatively pull the code out to a closure
let sharedIzyuk: (String) -> String = {
    krebf in
    // Place full codebase here, assuming it
    // runs under both 2.2 and 3
    return "sharedIzyuk \(krebf)"
}

#if swift(>=3.0)
    func izyuk(_ krebf: String) -> String {
        return "3.x " + sharedIzyuk(krebf)
    }
#else
    func izyuk(krebf: String) -> String {
        return "2.2 " + sharedIzyuk(krebf)
    }
#endif

However ugly, this approach enables you to isolate the shared functionality from the different signatures. You’ll only need to maintain one closure — even if that closure itself contains conditional compilation for 2.2 and 3 code.

The only other practical solutions are to commit to a full Swift 3 migration, to maintain separate 2.2 and 3 repositories, or to create parallel full implementations using #if swift() build configurations with duplicated code.

4 Comments

  • Why a closure instead of a function? It does not look as though it is being passed anywhere, or closing over variables I surrounding scope.

    • Because closures don’t have first parameter label rules

  • Of course. I need more coffee!

  • Or you could just go with the Swift 3.0 syntax
    func fripple(_ fweep: String)
    and live with the “extraneous ‘_’ in parameter” warning in Swift 2.2.