A question on syntax for arguments of a function and a closure

I am just a curious newbie learning Swift.

As in the following example, syntax for specifying arguments of a function and a closure are different.

func f(a: Int) -> Int {
    return a * 2
}
let c = { (a: Int) -> Int in
    return a * 2
}

f(a:3) // valid
// f(3) // invalid

//c(a:3) // invalid
c(3) // valid

I have two questions.

  1. Why is it designed this way? (If it's just a history, I want to know the historical background.)
  2. I know the syntax for function arguments was changed at Swift 3.0. Is there any possibility that will happen again for closure arguments?
2 Likes

You are contemplating SE-0111. This will help you perform searches in the forum archive.

Users frequently complain about the inability to name closure arguments (i.e. it is impossible to write f(a: 3) if f is defined as a closure). User complain because this breaks the object/closure symmetry, and makes the language irregular.

Objects (data with behaviors) and closures (behaviors that close over data) are supposedly roughly equivalent, which means that one should be able to freely choose between those two forms, for whatever reason deemed relevant:

// Form 1: sure, why not
protocol P { func f(_ x: Int) }
let object: P = ...
object.f(1)

// Form 2: sure, looks good as well
let closure: (Int) -> Void = ...
closure(1)

Unfortunately, this freedom is hindered by SE-0111 as soon as one wants to use argument labels. Only regular methods can have them, and closures are out of the game.

This creates developer frustration, because a developer often discovers too late that the closure path is hindered by SE-0111. After one has successfully toyed with closures that do not need argument labels, enjoyed the game, and started relying on closures, one suddenly hits a wall.

This explains why users frequently ask that argument labels in closures come back.

Now, asking is far from enough for the language to be changed (see the Swift Evolution Process), so nothing happens.

2 Likes

I, uh, object to this characterization. It’s frustrating that closures don’t get the benefits of argument labels, but closures and objects aren’t anywhere close to interchangeable anyway in Swift: objects have methods and closures do not. Even the original asker was comparing functions and closures.

In any case, it may help understanding to observe the following additional behavior:

var g = f
g(3) // not g(a: 3)
g = c
g(3) // still not g(a: 3)

Argument labels are part of a function’s name. Closures don’t have names; the lets and vars and parameters you put them in do. So if someone were to add argument labels for closures (which I support), it would be sugar for something like this:

let `c(a:)` = { … }
c(a: 3)

var `g(x:)` = f
g(x: 3)
g = c
g(x: 3)
10 Likes

Wait, would you really need the backticks there? I've spent this whole time imagining that the final syntax for closures with argument labels would be:

let f(x:) = { $0 * 2 }

I deliberately picked a very explicit syntax to make it clear what was going on; other syntaxes may be possible as well. The most important place for this is parameters anyway, and there I think we’d want to support interleaving the labels with the type, whether or not that was supported anywhere else.

Exploring this space further, the above could logically extend to:

let f(x: Int) -> Int = { x * 2 }

and then internal parameters may be added:

let f(with x: Int) -> Int = { x * 2 }

Similar to func, let f would be constant, contrary to func, let f could be initialised to different things:

let f(with x: Int) -> Int
if ... {
    f = { x * 2 }
} else {
    f = { x * 3 }
}

If this is unnecessary we can remove let and leave only var:

var f(x: Int) -> Int = { x * 2 } // can be reassigned
var f(x: Int) -> Int { x * 2 } // can't be reassigned

The latter is pretty much the "func" aside from the keyword.

A side note on computed vars.

As a side note on computed vars, I'd like to see a different keyword than "var" for them:

var x: Int {
   ...
}

var y: Int {
    get { ... }
    set { ... }
}

Not sure about the name, perhaps "computed" or something similar. It is very different from stored vars and deserves its own keyword.

I'd definitely enjoy being able to use argument labels with closures.

@tera, regarding your suggestion, would this be allowed?

let concat <T>([T]) -> (to: [T]) -> [T] =
    { x in
    { y in y + x }}

concat ([3, 4]) (to: [1,2])

I'd love to be able to do this very much! I personally have a tough time with deciphering function signatures when types are mixed into the signature. I also like being able to reason about types separately. Currently, it's not really feasible to avoid this in apps beyond simple toy apps.

The only thing this doesn't quite address is making function types more portable, right now in Swift, we can't reuse function types with the standard way of defining named functions. The below functions have the same type signature but there's not a way to share them with each other. And there's not really a way to do this when assigning closures to variables because we can't specify generics when calling that kind of function:

func filter<T>(by fn: (T) -> Bool, in someArray: [T]) -> [T] {
  return someArray.filter(fn)
}

func reject<T>(by fn: (T) -> Bool, in someArray: [T]) -> [T] {
  return someArray.filter({ item in !fn(item) })
}

It'd be great to be able to do this:

typealias myType<T> = ((T) -> Bool, [T]) -> [T]

let filter:myType = { fn, someArray in someArray.filter(fn) }
let reject:myType = { fn, someArray in someArray.filter { item in !fn(item) } }

// or if labels in the typealias is desired
typealias myType<T> = (_ fn: (T) -> Bool, to someArray: [T]) -> [T]

let filter:myType = { someArray.filter(fn) }
let reject:myType = { someArray.filter { item in !fn(item) } }

I think the ideas are complementary and would really help out developers like myself that find it hard to parse a function signature when everything is all mixed together. I'd be extremely happy with and helped out by either/or though!