Pouring Some Sugar On Closure Expressions

In Swift, functions are considered first class citizens. Does this mean they get more leg room, complementary olives and the first pass at life-rafts?…

“A real man makes his own luck” – Billy Zane (first class citizen on Titanic)

…no, what this means is that functions can be:

  • Assigned to variables and constants.
  • Passed as parameters into other functions.
  • Returned as arguments from other functions.

Closures are self contained blocks of functionality that can visit you in the night in 3 forms:

  • Global functions – Yes, your garden variety functions are considered closures. These functions are named and do not utilize the magical powers to capture surrounding variables.
  • Nested function – Functions can be placed within the statement body of global functions. They have a name and do make use of closures ability to capture the value of surrounding variables.
  • Closure expressions – These fancy fellows contain the building blocks of functions but don’t themselves have a name. They are able to capture values from the surrounding context.

The focus of this post is to look at the copious amounts of syntactic sugar that can be poured upon closure expressions.

Many of the function within the Swift standard library make use of closure expressions. Examples of this include the functions on arrays: .sort .map .reduce .filter etc…  Closure expressions are a flavor of closures where an anonymous function is passed as a parameter into another function. An amazing feature of this is that the closure expression has the ability to capture the values of surrounding variables and constants. This is known as closing over those variables and constants.

Closure expressions take the basic outline:

{ (parameters) -> return type in
statements
}

Having functions that accept functions as arguments has the potential to be verbose. Swift includes a number of ways to dramatically optimize the syntax to very short powerful expressions. This leads to more of a declarative style of programming where the code reflects the intention of what’s to be accomplished rather than showing line by line how it’s going to be accomplished.

Let’s take a look at the process of syntax optimization when using the .map function. The prototype for .map looks like this:

func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]

The map function accepts the transform closure of type (Self.Generator.Element) throws -> T . “T” refers to a generic type, meaning that map can work on collections of different types. -> [T] means that map will return an array of the same type as the one passed in, after the transform has been performed. The @noescape keyword makes it a “nonescaping closure”. This tells the compiler that the closure will definitely not be needed once map has been carried out.

We can prove that map works in this way by creating a function that takes an element, performs a function and returns the same type. Remember, because functions are first class citizens they can be assigned to a constant or variable, like so:

func upOctave(n: Int) -> Int {
    return n * 2
}

let upOctaveFunc = upOctave

let freqArray = [330, 247, 196, 147, 110, 82]

let newArray = freqArray.map(upOctaveFunc)

upOctave takes an integer, in this case frequencies of musical notes, doubles it and returns the value. This function is assigned to the constant upOctaveFunc. Note there are no parentheses when assigning the function (as opposed to when you’re calling the function). The constant is then passed in as an argument to .map, which uses it to return a new array where all the frequencies have been doubled.

Show Me the Sugar!

The same result can be achieved using a closure expression i.e. declaring the upOctave function straight inside the map function. Let’s start with the longest form of the closure expression and whittle it down.

Step 1 – Full Closure Expression

let arrayStep1 = freqArray.map({ (n: Int) -> Int in return n * 2 })

arrayStep1 // = [660, 494, 392, 294, 220, 164]

Note: The body of the closure expression is marked by the keyword in.

Step 2 – Take Away the Types

The map method is being called on an array of type Double. Swift can infer that the argument being passed in is of type Double -> Double.

let arrayStep2 = freqArray.map({ n in return n * 2 })

Step 3 – Loose the Return Keyword

If the closure is a single expression the return keyword can be omitted.

let arrayStep3 = freqArray.map({ n in n * 2 })

Step 4 – Who Need Arguments When You’ve Got $$$

Why end this mad syntactical-grapefruit-diet there when we can shed a few more keystrokes? The arguments in the closure are automatically given shorthand names $0, $1, $2 etc… If you refer to these shorthand names in the return of the closure the arguments can be omitted completely. Now the syntax is getting short!

let arrayStep4 = freqArray.map({ $0 * 2 })

Step 5 – Trailing Closures

What remains of our syntax can be cleaned up further by moving the closure outside of the parentheses.

let arrayStep5 = freqArray.map() { $0 * 2 }

Note: By convention closure expressions are always the last argument to the function.

Run it Back

Let’s go through the same process with a slightly more complicated closure expression. Below is an array containing the volume (in dB) of a number of music groups recorded from the front row of an outdoor music festival. Let’s say the front row is 10 meters from the stage. Now we’re going to use .map to output the volume of these sources from the back row. Let’s say that’s at 200 meters away.

Sound pressure level (in dB) are measured on a logarithmic scale as opposed to a linear scale. This is due to the massive range in human hearing as well as the fact that our perception of loudness is more aligned with a logarithmic interpretation. If you’ve made it to the end of this paragraph, congratulations! You’re still awake!

The formula for working out the noise reduction in dB at two different distances is:

NR = 20log(r2/r1)

The different forms of closure syntax would be as follows:

let frontRowVolume = [110.2, 99.4, 105.4, 100.1]

// step 1 - full closure expression

let backRowVolume = frontRowVolume.map({ (n: Double) -> Double in return n - (20 * log10(200/10)) })

backRowVolume  // [84.18, 73.38, 79.34, 74.08]


// step 2 - take away the types

let backRowVolume2 = frontRowVolume.map({ n in return n - (20 * log10(200/10)) })


// step 3 - loose the return keyword

let backRowVolume3 = frontRowVolume.map({ n in n - (20 * log10(200/10)) })


// step 4 - who needs arguments?

let backRowVolume4 = frontRowVolume.map({ $0 - (20 * log10(200/10)) })


// step 5 - trailing closures

let backRowVolume5 = frontRowVolume.map() { $0 - (20 * log10(200/10)) }

 

  • The Swift playground for this post can be found on GitHub here

Leave a Reply