iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🍕

Variadic Generics Are Coming in Swift 5.9

に公開
2

Swift 5.9 was introduced at WWDC23, and macros are attracting a lot of attention. At the same time, a new feature called Variadic Generics is arriving.

Variadic Generics are quietly enabling new types of APIs. In this article, I'll explain Variadic Generics in depth.

Variadic Generics

Normally, generics have a fixed number of type parameters. For example, let's consider creating a function debugOnlyPrint that outputs a value only in DEBUG builds. Here, one type parameter T is declared.

func debugOnlyPrint<T>(_ value: @autoclosure () -> T) {
    #if DEBUG
    print(value())
    #endif
}

// Works
debugOnlyPrint("Hello, Swift")

There can be multiple type parameters. Let's define debugOnlyPrint taking two arguments.

func debugOnlyPrint<T, U>(_ value0: @autoclosure () -> T, _ value1: @autoclosure () -> U) {
    #if DEBUG
    print(value0(), value1())
    #endif
}
// Works
debugOnlyPrint("Hello, Swift", 3.14)

However, to support three arguments, you need to define another function, and for four, yet another one. This makes you want a way to handle an arbitrary number of type parameters.

That's where Variadic Generics come in. By using Variadic Generics, you'll be able to handle an arbitrary number of type parameters.

Declaration

Variadic Generics are introduced with the each keyword and expanded with the repeat keyword. It might be hard to understand just by that, so let's first look at the declaration and call-side of the Variadic Generics version of debugOnlyPrint.

func debugOnlyPrint<each T>(_ value: repeat @autoclosure () -> (each T))

// Usage
debugOnlyPrint("Hi Swift", true, [1, 2, 3], -3.14, 256)

This each T is a concept called a "type pack," which, simply put, corresponds to a sequence of types. In the usage example, it's String, Bool, [Int], Double, Int. A type pack itself is not a type.

What's necessary to manipulate a type pack is repeat. It's easiest to understand if you think of it as an operation that maps a type pack to another type pack. By writing repeat @autoclosure () -> (each T), the above type pack is transformed into a different type pack: @autoclosure () -> String, @autoclosure () -> Bool, @autoclosure () -> [Int], @autoclosure () -> Double, @autoclosure () -> Int.

Calculation

Arguments declared with a type pack enter a "value pack." For example, by writing the following, you can execute each closure and obtain the results. repeat is used here as well, which can be thought of as a map from a value pack to another value pack.

repeat (each value)()  // "Hi Swift", true, [1, 2, 3], -3.14, 256

Additionally, value packs and type packs are sometimes collectively referred to as "parameter packs." According to Apple, singular forms are preferred over plural forms when naming parameter packs.

Constraints

You can apply constraints to type packs in several ways. First is the method of applying constraints with a protocol at the time of declaration.

func takeComparable<each T: Comparable>(...)

Additionally, there are cases where you want to require all types in a type pack to be the same type. In this case, you can use where and a dummy type parameter.

// Element becomes a dummy type parameter
func maxes<each S: Sequence, Element>(sequence: repeat each S) -> (repeat (each S).Element) where repeat (each S).Element == Element

// each S is [Int], Set<Int>, and Element is Int
maxes(sequence: [1, 2, 3], Set([832, 1, 42])) // (3, 832)

To constrain two type packs to be the same length, you simply provide a type that requires them to be the same length.

// When taking a tuple of T and U, the length constraint is implicitly inferred
func foo<each T, each U>(pair: repeat (each T, each U))

Relationship with Tuples

Type packs and tuples are similar but different. Tuples can be used as types in various situations, but type packs are not exposed as a language feature.

However, it is easy to handle type packs with tuple types, and value packs with tuple values together. In fact, writing (repeat each value) makes it a tuple, and writing (repeat each T) makes it a tuple type.

Conversely, it is also possible to apply repeat to a tuple like (repeat each value).

func take<each T>(tuple: (repeat each T)) {
    debugOnlyPrint(repeat each tuple)  // "Hi Swift", true, [1, 2, 3], -3.14, 256
}

// Usage
take(tuple: ("Hi Swift", true, [1, 2, 3], -3.14, 256))

Differences from Variadic Arguments

Actually, "variadic arguments" have been available in Swift up to now.

func foo(values: Int...) {
    print(values)
}

foo(values: 1, 2, 3, 4)  // [1, 2, 3, 4]

With variadic arguments using ..., basically only one type can be used. Furthermore, they are treated as an array internally. Although called "variadic arguments," in reality, they were just "syntactic sugar for functions that take an array."

For example, while print appears at first glance to be a function that takes variadic arguments of arbitrary types, its actual implementation is Any....

func print(_ values: Any..., separator: String = " ", terminator: String = "\n")

In contrast, variadic generics allow for processing arbitrary types without losing type information. Also, since they are not automatically converted into an array, they can be used more efficiently if you want to handle them in different collections, such as a Set. Finally, modifiers like inout and @autoclosure can only be used with variadic generics.

While most things possible with variadic arguments can also be done with variadic generics, there is one key difference. A function taking variadic arguments can still accept variadic arguments when converted to a closure. On the other hand, because a variadic generic function is resolved to a specific length and type at the call site, it can only be converted into a closure with a specific signature.

func foo(values: Int...)
let closure: (Int...) -> () = foo // ok
// ok
closure(1, 2, 3, 4, ...)

func bar<T>(value: repeat each T) where repeat each T == Int
let closure1: (Int) -> () = bar // ok
let closure2: (Int, Int) -> () = bar // ok
let closure7: (Int, Int, Int, Int, Int, Int, Int) -> () = bar // ok
let closure: (Int...) -> () = bar // not ok

Implementation of debugOnlyPrint

However, fully implementing debugOnlyPrint takes a bit more effort. This is because print doesn't work as expected.

func debugOnlyPrint<each T>(_ value: repeat @autoclosure () -> (each T)) {
    #if DEBUG
    repeat print((each value)())
    #endif
}

debugOnlyPrint(1, true, "Hi")
// We expect the output to be "1 true Hi", but it actually results in line breaks
// 1
// true
// Hi

Therefore, we explicitly write the first parameter and change the terminator (the character attached to the end).

func debugOnlyPrint<First, each T>(_ firstValue: @autoclosure () -> First, _ value: repeat @autoclosure () -> (each T)) {
    #if DEBUG
    func printWithPrefixSpace<U>(value: U) {
        print(" ", terminator: "")    // Output a space
        print(value, terminator: "")  // Output the value
    }
    print(firstValue(), terminator: "")
    repeat printWithPrefixSpace(value: (each value)()) // Output "space + value string" for each value
    print()   // Output the final newline
    #endif
}

debugOnlyPrint(1, true, "Hi")
// The output is now correctly "1 true Hi"

In reality, the implementation became a bit complex, but it's still much better than defining countless overloads.

Creating Variadic Generic Types

The above covers the basic usage of variadic generics, but you can also create variadic generic types.

struct VStack<each ContentView: View> {}

Internally, you can implement various functions using packs in the same way as with functions.

Implementation Techniques

repeat basically only handles map-like operations, but you can also perform reduce-like logic by executing a mutating function.

For example, let's create a function count that counts the number of variadic arguments.

func count<each T>(value: repeat each T) -> Int {
    var count = 0
    func increment(trigger _: some Any) {
        count += 1
    }
    repeat (increment(trigger: each value))
    return count
}

Here, we call the increment function by passing each value inside repeat. The specific value of each value doesn't matter; what's important is the count += 1 executed in the callee. By incrementing the value once for each item, count accumulates the number of times the function was called. Returning this value allows us to count the variadic arguments.

Using this approach, functions like equal can also be easily implemented.

func equal<each T: Equatable>(left: repeat each T, right: repeat each T) -> Bool {
    var result: Bool = true
    func mutatingCompare<U: Equatable>(left: U, right: U) {
        if !result {
            return
        }
        result = left == right
    }
    repeat (mutatingCompare(left: each left, right: each right))
    return result
}

While we use if !result here, another option is to use throw.

enum EqualError: Error { case notEqual }
func equal<each T: Equatable>(left: repeat each T, right: repeat each T) -> Bool {
    func checkEqual<U: Equatable>(left: U, right: U) throws {
        if left != right {
            throw EqualError.notEqual
        }
    }
    do {
        repeat (try checkEqual(left: each left, right: each right))
    } catch {
        return false   
    }
    return true
}

There seems to be plenty of room for design choices in this area.

Similarly, you can implement functions like lessThan.

enum LessThanError: Error { case greaterOrEqual }

func lessThan<each T: Comparable>(left: repeat each T, right: repeat each T) -> Bool {
    var result: Bool? = nil
    func mutatingCompare<U: Comparable>(left: U, right: U, result: inout Bool?) {
        if result != nil {
            return
        }
        result = if left < right {
            true
        } else if left > right {
            false
        } else {
            nil
        }
    }
    repeat (mutatingCompare(left: each left, right: each right, result: &result))
    return result == true
}

With this, we can achieve quite useful functionality. Let's implement sorted(with:), which takes multiple closures as arguments and evaluates them in order to sort the sequence.

public extension Sequence {
    /// Returns a sorted array of the sequence's elements with the given closures.
    ///  ```swift
    ///  // you can path keyPaths (But now it is not recommended and can cause runtime crash. See the warning)
    ///  let result = array.sorted(with: \.firstKey, \.secondKey)
    ///  // you can also path closures
    ///  let result = array.sorted(with: { -$0.firstKey }, \.secondKey, \.thirdKey, { -$0.forthKey })
    ///  ```
    /// - Parameter key: keys to sort on.
    /// - Returns: A sorted array of the sequence's elements.
    /// - Warning: This implementation in Swift 5.9 is working in a very fine balance with the compiler's mood. It seems there's a bug on compiler treatment of KeyPaths as functions, so currently it is not recommended to use KeyPaths on this function.
    @inlinable
    @_disfavoredOverload
    func sorted<each T: Comparable>(with key: repeat (Element) -> each T) -> [Self.Element] {
        func mutatingCompare<U: Comparable>(left: U, right: U, result: inout Bool?) {
            if result != nil {
                return
            }
            result = if left < right {
                true
            } else if left > right {
                false
            } else {
                nil
            }
        }

        return self.sorted { left, right in
            var result: Bool? = nil
            repeat (mutatingCompare(left: ((each key)(left)), right: ((each key)(right)), result: &result))
            return result == true
        }
    }
}

// Usage
struct User {
    var name: String
    var age: Int
    var joinedDate: Date
}

var users: [Users] = [...]
let result = users.sorted(with: {$0.name}, {$0.age}, {$0.joinedDate})

In this way, although it's a bit of a workaround, you can implement very convenient functions.

Known Issues

Swift 5.9 has not yet been officially released. As of Xcode 15 beta 1, the following limitations exist:

  • @autoclosure causes an error on the call side
  • repeat for tuples is not yet implemented
  • Variadic Generics do not work with KeyPath
  • Constraints using associated types do not work

In fact, some of the source code introduced in this article did not work when tested with Xcode 15 beta 1. Therefore, judging that these are likely bugs on the language side based on the specifications, I have included them as usage examples.

Summary

As introduced above, using Variadic Generics allows for creating more flexible APIs. Many features related to Variadic Generics are currently under active discussion, and along with macros, it's an exciting area to watch for future developments.

The source code introduced this time is also available at the link below. I've been playing around with various things, so feel free to take a look.

https://github.com/ensan-hcl/VariadicGenericsUtils

References

Discussion

swifttyswiftty

参考になります!
記事中の例で質問があります。

let closure1: (Int) -> () = bar // ok
let closure2: (Int, Int) -> () = bar // ok
let closure7: (Int, Int, Int, Int, Int, Int, Int) -> () = bar // ok
let closure: (Int) -> () = bar // not ok
https://zenn.dev/en3_hcl/articles/fd653e12e3b063#可変長引数との違い

この最後の closure は別の例のタイポでしょうか?もしくは単純に不要な行だったりしますか?

MiwaMiwa

ごめんなさい!Typoです。書きたかったのは(Int...) -> ()でした。修正しておきます!