While I was looking at examples of EDSLs while considering the comma elision proposal I hit upon another idea that would open up some interesting possibilities for EDSLs. The idea is to significantly broaden the scope of what can be done with variadic parameters by lifting the requirement that variadic arguments be turned into an Array
. Even more interesting is expanding variadics further to include labeled variadic arguments which are treated as a dictionary literal.
Here is a simple example of what labeled variadics can do in conjunction with comma ellison:
enum ColumnType { case int, string, ... }
struct Table {
init(_ columns: variadic [String: ColumnType]) { ... }
}
Table(
firstColumn: .int
secondColumn: .string
)
// without labeled variadics and comma ellision:
Table([
âfirstColumnâ: .int,
âsecondColumnâ: .string
])
It is hard to argue that the additional syntax in the latter example aids clarity. In fact, it is pretty easy to argue the opposite: the additional syntax gets in the way.
This syntax for declaring a table scheme is extremely lightweight. It is
I will keep an updated copy of the proposal at this gist. I am also including the initial draft below.
Enhanced Variadic Parameters
- Proposal: SE-NNNN
- Authors: Matthew Johnson
- Review Manager: TBD
- Status: Awaiting implementation
Introduction
This proposal enhances variadic parameters by introducing support for user-defined types, labeled variadic parameters, and a choice at the call site of whether to provide a variadic argument list or a collection value.
Swift-evolution thread: Discussion thread topic for that proposal
Motivation
Variadic parameters are a nice feature in Swift. They allow us to elide array brackets at a call site. Unfortunately this elision comes with a cost: even though variadics formally take an Array
one cannot be provided at a call site. This often results in an overload set to support direct arrays as well:
func compute(ints: Int...) -> Int {
return compute(ints: ints)
}
func compute(ints: [Int]) -> Int { ... }
Further, Array
is priveleged as the only type that is supported by variadic parameter syntax. This is at odds with Swift's otherwise pervasive support for user-defined types in its literal syntax.
Proposed solution
We should reimagine variadic parameters as simple syntactic sugar for optionally eliding collection literal brackets at the call site.
Syntax
Swift magically turns the current T...
type syntax into [T]
as the type used in the body of a function. Having the Array
assumption baked directly into this syntax means it will not work with other types.
In order to support types beyond Array
the variadic
parameter modifier is used:
func compute(ints: variadic [Int]) -> Int {...}
// callable in any of these ways:
compute(ints: 42, 43, 44)
compute(ints: [42, 43, 44])
compute(ints: arrayOfInt)
ExpressibleByArrayLiteral
All ExpressibleByArrayLiteral
types are supported. The compiler will use the type's init(arrayLiteral:)
initializer to create a value from variadic arguments provided at the call site.
func compute(ints: variadic Set<Int>) -> Int {...}
// callable in any of these ways:
compute(ints: 42, 43, 44)
compute(ints: [42, 43, 44])
compute(ints: setOfInt)
ExpressibleByDictionaryLiteral
ExpressibleByDictionaryLiteral
types are also supported with a variadic list of labeled arguments . The labels in this list are key literals and the arguments are values in a dictionary literal.
In order to use the type's init(dictionaryLiteral:)
initializer arguemnt labels must be turned into values of type Key
. This proposal includes two ways to do that.
ExpressibleByStringLiteral keys
When the key is ExpressibleByStringLiteral
, the argument label can be passed to init(stringLiteral:)
to create a value of type Key
.
func compute(pairs: variadic [String: Int]) -> Int { ... }
// callable in any of these ways:
compute(first: 42, second: 43, third: 44)
compute(pairs: ["first": 42, "second": 43, "third": 44])
compute(pairs: dictionaryFromStringToInt)
Notice that this sugar allows not only the dictionary literal brackets to be ellided, but the string quotes and the eternal pairs
label as well. pairs
is only used when a collection value is passed directly and variadic syntax is not used.
This feature is not limited to Dictionary
:
// assume OrderedDictionary: ExpressibleByDictionaryLiteral
func compute(pairs: variadic OrderedDictionary<String, Int>) -> Int { ... }
// callable in any of these ways:
compute(first: 42, second: 43, third: 44)
compute(pairs: ["first": 42, "second": 43, "third": 44])
compute(pairs: orderedDictionaryFromStringToInt)
Enum keys
When the key is of an enum
type that does not have any associated values keys can be required to match the identifiers of its cases.
enum Foo { case foo, bar, baz }
func compute(pairs: variadic [Foo: Int]) -> Int { ... }
// callable in any of these ways:
compute(foo: 42, bar: 43, baz: 44)
compute(pairs: [.foo: 42, .bar: 43, .baz: 44])
compute(pairs: dictionaryFromFooToInt)
Notice that this sugar allows not only the dictionary literal brackets to be ellided, but the dot prefix on the enum case identifier and the eternal pairs
label as well. pairs
is only used when a collection value is passed directly and variadic syntax is not used.
This feature is not limited to Dictionary
:
enum Foo { case foo, bar, baz }
func compute(pairs: variadic OrderedDictionary<Foo, Int>) -> Int { ... }
// callable in any of these ways:
compute(foo: 42, bar: 43, baz: 44)
compute(pairs: [.foo: 42, .bar: 43, .baz: 44])
compute(pairs: orderedDictionaryFromFooToInt)