Introduction
Lately, I have spent some time sharpening my Swift skills. Swift is a wonderful language, but it has some aspects that deserve attention.
The first topic I wanted to review is Closures.
Closures are a powerful feature offered by the Swift language. You can use them to pass functions to other functions, resulting in a very flexible code.
Never forget what Voltaire said: "With great powers come great responsibilities" (no, it wasn't Uncle Ben, my dear Marvel fanboy...) - irresponsible usage of closures may lead to non-maintainable code.
Definition
So, what are Closures? According to the Swift Programming Language official documentation (https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures):
Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.
In short, this means that closures are functions. Actually, according to the documentation, functions are special cases of closures. The documentation considers functions as closures baptised with a name. The most generic syntax of a closure is:
{ (param1, param2, ..., paramN) [-> ReturnType] in
//actual closure code
}
in the previous definition, the ReturnType
is optional. This is coherent with how functions are defined. The parameter list is optional as well, as we will shortly show.
Closures as variables
In Swift, the following declaration is completely legal, and will compile:
var closure1 = { print("I am a closure, and I am a variable") }
This variable contains a function that can be invoked:
closure1()
This would output:
I am a closure, and I am a variable
Being a variable, we can modify it:
closure1 = { print("Being a variable, I can be modified") }
Now the output would be:
Being a variable, I can be modified
Parameters
Let's add some more spice: input parameters. We want to create a function that accepts a number and prints its square. The code is quite intuitive:
let square = {(number: Int) in print(number*number) }
square(2)
The output is 4
, as expected. As a point of interest, observe that closures can be also defined as constants.
Closures
with parameters and return types If we wanted to have the value returned instead of having it only printed, we'd need to redesign the closure:
let squareR = {(number: Int) -> Int in
return (number*number)
}
The invocation becomes:
let s = squareR(10)
print(s)
Closures without parameters but with return types
The last case we have is the one in which a closure doesn't accept any parameter, but returns a value. So, we write a function that returns the current date, formatted in the European way:
let currentDate = { () -> String in
let date = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"
return(dateFormatter.string(from: date))
}
Parameter inference
Swift infers the type of the parameters, hence the following code is perfectly legal:
let positive = {x in return x>0.0}
Here, the overhead of the (x: Double)
has been simplified, because the variable is compared with a double. Hence the instruction
let minusPI = -3.1415
print(positive(minusPI))
would return true
, but the following won’t compile:
let four = 4 print(positive(four))
This code would try to compare the variable four
, which is an integer, with a double (0.0
). The console would report error: cannot convert value of type 'Double' to expected argument type 'Int'
.
The very first thing that came into my mind when I saw this feature was to write something like:
let product = {x,y in x*y}
Unfortunately, this won’t work, because the compiler cannot understand if that *
represents the operation between Integers or Doubles. This makes perfect sense, from an Assembly perspective.
PERLish arguments
In Swift, the declarations:
let positive = {x in return x>0.0}
and
let positive = {$0>0.0}
are equivalent. In Swift, $0
refers to the first argument, $1
to the second, and $n
to the (n+1)-th. Welcome back, PERL.
Closures as function parameters
Closures can be used as variables and constants, and consequently, they can be used as function parameters. This is consistent with the structure of functions.
The following code compiles and works as expected:
let rnd = {x in
return Int.random(in: 1..<x)
}
let IntSqrtRoot = {(x: Int) in
let d = Double(x)
let r = Int(d.squareRoot())
return r
}
func F(Closure1 C1: (Int)->Int, Closure2 C2: (Int)->Int, aRandomInteger n: Int) {
let val1: Int = C1(n)
let val2: Int = C2(n)
print("A random number less than (n): (val1)")
print("The integer part of the square root of (n) is (val2)")
}
F(Closure1: rnd, Closure2: IntSqrtRoot, aRandomInteger: 17)
The output of this code varies because of the randomness of the first closure, but in general, it has the following structure:
A random number less than 17: 13
The integer part of the square root of 17 is 4
The type for the closures, in this case, is (Int)->Int
, but they may vary.
Closures as return types
We may have a function returning a closure. The definition of such a function can be as follows:
func G(switch n:Int) ->(Int)->Any{
let f1 = {x in
return x+1
}
let f2 = {(x:Int) in
let d = Double(x)
return d.squareRoot()
}
let f3 = {(x:Int) in
var result = ""
for _ in 0..<x{
result = "(result)*"
}
return result
}
switch n{
case 0:
return f1
case 1:
return f2
case 3:
return f3
default:
return {(x:Int) in
return ("10","ten")
}
}
}
We can invoke it as follows:
let function1 = G(switch: 1)
print(function1(123))
let function2 = G(switch: 2)
print(function2(10))
let function3 = G(switch: 3)
print(function3(17))
let function4 = G(switch: 0)
print(function4(10))
this returns the following output:
11.090536506409418
("10", "ten")
**********
11
A word of caution: these two features, taken altogether, can lead to spaghetti code, ravioli code, macaroni code, and to lasagna code. So be very cautious when you use it - you need to watch your daily carbs consumption :)
Trailing closures
There is another way to invoke functions accepting closures as parameters. This happens when the closure is the final argument.
Assume we have this function:
func generateHash(plaintext t: String, seed s: Int, algorithm a: (String, Int) -> String) -> String{
let encypheredText = a(t,s)
return encypheredText
}
We may invoke it as:
let encrypted1 = generateHash(plaintext: "Pen Pineapple Apple Pen", seed: 321, algorithm: {
(plaintext: String, seed: Int) in
return "This is the hash of '(plaintext)' encrypted with TripleDES! Key used is (seed)"
}
)
print(encrypted1)
or we could define a variable with a closure implementing the algorithm:
let RSA = {(plaintext: String, seed: Int) in
return "This is the hash of '(plaintext)' encrypted with RSA! Key used is (seed)"
}
print(generateHash(plaintext: "Pen Pineapple Apple Pen", seed: 123, algorithm: RSA))
There is another way to invoke this function. The closure is (explicitly) written outside the closing parentheses, as follows:
let encrypted2 = generateHash(plaintext: "Pen Pineapple Apple Pen", seed: 456){(plaintext: String, seed: Int) in
return "This is the hash of '(plaintext)' encrypted with ECC! Key used is (seed)"
}
I have not been able to do this kind of invocation with two different closures (e.g., f(params){closure1}{closure2}
, but that’s probably for the best. Personally speaking, I don’t find this feature elegant. Nevertheless, it’s important we know it’s there.
Commentary
This covers the basic functionalities of Closures.
We have shown how closures constitute a great way to write flexible code. They are widely used throughout AppKit exactly for that flexibility, but their practical application goes far beyond building an interface - for instance, AI or dynamic analysis tools can largely benefit from closures.
The only word of caution is be wary of how you use closures! Writing spaghetti code (or better, cannelloni code. This stuff is filled!) is the real risk, here.