Language syntax edge case: ExpressibleByArrayLiteral and VarArgs don't play nice together

I ran into this when I was playing around with an OptionSet. Take this sample OptionSet from the documentation:

struct ShippingOptions: OptionSet {
    let rawValue: Int

    static let nextDay    = ShippingOptions(rawValue: 1 << 0)
    static let secondDay  = ShippingOptions(rawValue: 1 << 1)
    static let priority   = ShippingOptions(rawValue: 1 << 2)
    static let standard   = ShippingOptions(rawValue: 1 << 3)

    static let express: ShippingOptions = [.nextDay, .secondDay]
    static let all: ShippingOptions = [.express, .priority, .standard]
}

You can use its conformance to ExpressibleByArrayLiteral (which comes from OptionSet, which conforms to SetAlegrba, which conforms to it) to compose options:

func f(singleArg: ShippingOptions) {
    print(type(of: singleArg), singleArg, singleArg == .express)
    // prints `ShippingOptions ShippingOptions(rawValue: 3) true`
}


f(singleArg: [.nextDay, .secondDay])

Even though var args are similar to an array literal syntax, they're not an array literal. The elements passed to a T... are always assembled together into an Array<T>, bypassing any conformance to ExpressibleByArrayLiteral. That leads to this understandable, but undesirable result:

func f(optionSetVarArgs: ShippingOptions...) {
    print(type(of: optionSetVarArgs), optionSetVarArgs)
    // prints `Array<ShippingOptions> [ShippingOptions(rawValue: 1), ShippingOptions(rawValue: 2)]
`
}

f(optionSetVarArgs: .nextDay, .secondDay)

You can workaround this by passing the ShippingOptions... value through the SetAlgrebra.init<S>(S) initializer: ShippingOptions(optionSetVarArgs)

OptionSet is a curious case, in that's its a type that models both individual values, and a collection of them, all under one roof.

Even if varargs constructed values through ExpressibleByArrayLiteral conformances, you couldn't really use it to construct var args that were a Set<T>. How would the compiler know you want a Set and not an Array?

I think this is behaving as designed. Variadic arguments aren't array literals and there's no documentation that suggests they go through ExpressibleByArrayLiteral. There are plenty of reasons why the current varargs situation isn't great, but it's always been the element type you put in the declaration, not the collection type. It's just a "coincidence" of OptionSet that those two types are the same.

Hey Jordan!

Indeed, I acknowledge that:


Yep:

I guess I'm objecting to the "undesirable" bit. I suppose if we had a way to declare the collection type used for VarArgs, we might use ExpressibleByArrayLiteral to do it, but I'm not 100% sure that's the case. On the other hand, if that's what you want to talk about, calling this a "syntax edge case" seems weird. There's no "edge case" here.

2 Likes

True. I don't know of a better succinct term to describe this, though.