Trailing collection literals

(Matthew Johnson) #1

I'm a huge fan of Swift's trailing closure syntax. It is much nicer than moving the closing ) to the end of the closure. This is not just an aesthetic difference: the closing ) signals to a reader that the argument list is complete with the closure. Without trailing closure syntax readers have to glance to the end of the closure to know if additional arguments are provided. This syntax is especially useful when the closure has several lines, as is common in callbacks.

Continuing the recent trend of discussing EDSLs, we should provide similar syntactic sugar for collection literals when the last argument in a parameter list is ExpressibleByArrayLiteral or ExpressibleByDictionaryLiteral. This sugar will be especially useful for DSLs that model trees and therefore have "children" of some kind.

struct View {
    let frame: CGRect
    // ...
    let subviews: [View] = []
}

let view = View(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) [
    View(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) [
        View(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
    ],
    View(frame: CGRect(x: 20, y: 20, width: 10, height: 10))
]

Note, because it would conflict with subscripts paraenthases may not be omitted as they can with closures.

func foo(ints: [String]) { ... }
foo() [ // the parentheses here are required
    "hello",
    "world"
]

Dictionary literals would also be supported:

enum ColumnType { case int, string }
struct Table {
    let name: String
    // ...
    let columns: OrderedDictionary<String: ColumnType> = [:]
}

let table = Table(name: "MyTable") [
    "column1": .string,
    "column2"; .int
]
1 Like
(Adrian Zubarev) #2

What will you do in case of foo(_: [Int] = []) -> [Int]?

What does this mean?

foo()[0] // trailing literal or subscript call on resulting array?
6 Likes
#3

It still conflicts. The following is valid Swift today, it contains your example verbatim at the bottom, and it calls the other foo function (which returns a Foo) then subscripts the result:

struct Foo {
  subscript(_ s: String...) -> Int {
    return s.count
  }
}

func foo() -> Foo { return Foo() }

func foo(ints: [String]) { }

foo() [    // Calls the ()->Foo overload and subscripts the result
  "hello",
  "world"
]
1 Like
(Matthew Johnson) #4

Ahh, you're right. I obviously didn't think this one through all the way.

1 Like
#5

What if it could be written as variadic parameter instead?

let view = View(frame: CGRect(x: 0, y: 0, width: 100, height: 100),
                subviews: View(frame: CGRect(x: 0, y: 0, width: 10, height: 10),
                               subviews: View(frame: CGRect(x: 0, y: 0, width: 1, height: 1))),
                          View(frame: CGRect(x: 20, y: 20, width: 10, height: 10)))
(Matthew Johnson) #6

Of course a variadic is possible. Unfortunately it requires the closing parentheses to be placed after the subview list. This isn't so bad. But it would be nice if we had the option to move the literal outside the call like we do with closures. Unfortunately that won't work - as was pointed out already it conflicts with subscript syntax.

(Adrian Zubarev) #7

You could require an extra character to communicate your intent.

foo(): [
  "Hello",
  "World"
]

That wouldn‘t be a deal breaker for me as it barely adds any visual noise.

Feel free to proof that there is code that will not work here as well. We could use something else instead of :.

(Frederick Kellison-Linn) #8

What about:

func foo(string: [String] = []) -> Int { return 0 }

let things = [foo(): ["Hello", "World"]] // [Int] or [Int: [String]]?
2 Likes