Swift Parameter Packs

Parameter packs are new in Swift 5.9 and they are pretty cool. Here’s a few example uses from an initial exploration.


valuesAt

I often want to pluck out specific values from a type but don’t necessarily want to do it over multiple lines e.g. plucking details out of a Github pull request might look like this

let number = pullRequest.number
let user = pullRequest.user.login
let head = pullRequest.head.sha

The above could go on further with many more parameters but the main take away is it creates some repetitive noise and sometimes I just want a one liner. With parameter packs we can write a valuesAt function to achieve this result

let (number, user, head) = valuesAt(pullRequest, keyPaths: \.number, \.user.login, \.head.sha)

The end result is the same in that I have 3 strongly typed let bindings but I can get it onto one line.

The implementation of valuesAt looks like this:

func valuesAt<T, each U>(
    _ subject: T, 
    keyPaths keyPath: repeat KeyPath<T, each U>
) -> (repeat each U) {
    (repeat (subject[keyPath: each keyPath]))
}

decorateAround

With higher order functions in Swift it’s easy to write a function that decorates another. The issue is handling varying arity and differing return types means we previously had to write loads of function overloads. With parameter packs we can write a generic function that allows us to write wrappers inline.

Imagine I need to log the arguments and return value for a function but I don’t have access to the source so I can’t just modify the function directly. What I can do is decorate the function and then use the newly generated function in the original functions place.

let decoratedAddition: (Int, Int) -> Int = decorateAround(+) { add, a, b in
    let result = add(a, b)
    print("\(a) + \(b) = \(result)")
    return result
}

print(decoratedAddition(1, 2))

//=> 1 + 2 = 3
//=> 3

With the above the core underlying function is unchanged but I’ve added additional observability. With this particular set up the decorateAround actually gives the caller lots of flexibility as they can also optionally inspect/modify the arguments to the wrapped function and then modify the result.

The code to achieve this triggers my semantic satiation for the words repeat and each but here it is in all its glory

func decorateAround<each Argument, Return>(
    _ function: @escaping (repeat each Argument) -> Return,
    around: @escaping ((repeat each Argument) -> Return, repeat each Argument) -> Return
) -> (repeat each Argument) -> Return {
    { (argument: repeat each Argument) in
        around(function, repeat each argument)
    }
}

We could go further and create helpers that make it simple to decoratePre and decoratePost and only use the decorateAround variant when we need full flexibility.


memoize

With the general pattern of decoration there are other things we can expand on. One such function would be to memoize expensive computations so if we call a decorated function with the same inputs multiple times we expect the computation to be performed only once. One example might be loading a resource from disk and keeping it in a local cache to avoid the disk IO when the same file is requested

let memoizedLoadImage = memoize(loadImage)

memoizedLoadImage(URL(filePath: "some-url"))
memoizedLoadImage(URL(filePath: "some-url"))

memoizedLoadImage(URL(filePath: "other-url"))

In the above example the image at some-url will only have the work performed to load it the first time, on the subsequent call the in memory cached result will be returned. The final call to other-url will not have any result in the cache and so would trigger a disk load.

In order to build this one we have to get a little more inventive with things as the cache is a Dictionary so we need to build a key somehow but tuples are not Hashable. I ended up building an array for the key that has all the arguments type erased to AnyHashable. The code looks like this:

func memoize<each Argument: Hashable, Return>(
    _ function: @escaping (repeat each Argument) -> Return
) -> (repeat each Argument) -> Return {
    var storage = [AnyHashable: Return]()
    
    return { (argument: repeat each Argument) in
        var key = [AnyHashable]()
        repeat key.append(AnyHashable(each argument))
        
        if let result = storage[key] {
            return result
        } else {
            let result = function(repeat each argument)
            storage[key] = result
            return result
        }
    }
}

Conclusion

Parameter packs are an interesting feature - I’m not sure the above code snippets are particularly good or even sensible to use but I hope it helps people get their toe in the door on using the feature and potentially coming up with stronger use cases than I’ve imagined.