Help understand the trailing closures syntax

Hi,

Found an interesting syntax and approach to iterate a list and call a method in each item. From the following source code example available in the AudioKit repo ( AudioKit/SongProcessor.swift at 292ccaccdb8680509fff0c40aa993572b8d85fc4 · AudioKit/AudioKit · GitHub ), as follows:

func playLoops(at when: AVAudioTime? = nil) {
    let startTime = when ?? SongProcessor.twoRendersFromNow()
    playersDo { $0.play(at: startTime) }
}

func playersDo(_ action: @escaping (AKAudioPlayer) -> Void) {
    for player in players.values { action(player) }
}

I'm still very fresh in Swift, but it looks familiar! The playLoops calls the playersDo without parentheses but pass a closure as an argument $0.play(...). Seem to be called trailing closures syntax? Can we pass other arguments the same way?!

Finally, when the control flow runs over the players.values list it calls the action, which corresponds to the closure:

$0.play(at: startTime)

I need to wrap my head around the concept of trailing closures and no parentheses that are a bit difficult to manage at the moment, is this correct?

Thank you!

EDIT: Found the following video that explains it, apologises for asking Trailing closure syntax – Swift in Sixty Seconds - YouTube

Ok, so after looking around for a bit it seems that the syntax helps to keep closures more legible when a closure accepts an argument, for example. The use of $0, is the syntax sugar for the first passed parameter rule or syntax sugar, so both put together we get the example posted above! Otherwise, an example of this would be something like:

func messenger (action: (String) -> void) {
  action("Hello world!")
}

messenger {
  print("The message is \($0)")
}

Where a func has a parameter called action that receives a single parameter String and returns void, in the inner body calls the param action with the argument "Hello world!". The messenger function is invoked with trailing closure, where $0 is the hard typed "Hello world". Correct?

You can start with the fully featured call, the remove what the compiler can infer. Starting with calling messenger with function name.

// Call `messenger` with function
func someAction(input: String) {
  print("The message is \(input)")
}
messenger(action: someAction(input:))

On to the fully featured equivalent closure:

// Call with equivalent *fully featured* closure
messenger(action: { (input: String) -> Void in print("The message is \(input)") })
// Compiler knows the return type, drop it
messenger(action: { (input: String) in print("The message is \(input)") })
// Argument type as well
messenger(action: { (input) in print("The message is \(input)") })
// Unnecessary parenthesis
messenger(action: { input in print("The message is \(input)") })
// The last argument (input) is used,
// we can switch to use `$0`, `$1`, etc., to refer to arguments, starting with `$0`
messenger(action: { print("The message is \($0)") })

That's as terse as current syntax can be for the closure, now, onto the trailing call.

// Last argument is closure, move closure outside, and drop the argument
messenger() { print("The message is \($0)") }
// No other argument in the parenthesis, drop it
messenger { print("The message is \($0)") }

Since closure is the call-site syntax, they tend to be very terse, or allow for very terse syntax. Note that all syntaxes above are valid function call syntax.

5 Likes

That's brilliant @Lantua! Appreciate it ; )