Monday, September 17, 2018

Swift 4 Introduction Series 1.13 - Closure

Closure

Closure are snippet of code that can be pass around using function. Closure are similar to blocks in C and Objective C and it is similar to lambdas in other programming language. Another characteristics of closure is that it is able to capture values and reference value from the context which they are defined.


  • Closure is similar to function. In fact a function is considered as a closure with name but did not capture any values.
  • Nested functions are closure that have a name and can capture values in the enclosing function.
  • Closure is function without name and they can capture value from their surrounding context.
  • Closure in Swift also supported optimization features and inference such that it saves us from laborious coding.


Closure Expression

Closure expression syntax is as follows:
<function_that_take_closure>.(<clsoure_parameter>:{ ( <parameters> ) -> <return type> in
<set_of_instructions>
})


Additional Rules:
  • Parameters in closure can be in-out parameters.
  • It cannot have default value.
  • It can take variadic parameter provided it is named.
  • Tuples can also be used as parameter and return type.


Notes:
  • Please note that the parameters must be declared within braces {}.
  • The keyword ("in") tell the system that the instructions start after in.


Using Sorted Closure

When we deal with dictionary and array, we encounter a function sorted(by:). This function supports closure. Therefore, we will be using this sorted function as an introduction to closure.


Since function is a closure with name. This means that the closure also accept function. The sort function in array or dictionary, will perform the actual sorting. What we need to provide is the sorting criteria. The sorting criteria can be a function by itself.


Sorting array example:


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA"]

// Create function to provide sorting criteria
func sortAsc(string1: String,string2: String) -> Bool {
   return string1 < string2
}

func sortDesc(string1: String,string2: String) -> Bool {
   return string1 > string2
}

// Implement sorting
let sortedAsc = someArray.sorted(by: sortAsc)
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArray.sorted(by: sortDesc)
print("Sorted descending: \(sortedDesc)")




We can convert the previous example to closure. What we need to do is to copy the function without the name, change the position of opening braces and add in the keyword "in". Finally, we can tidy up the code into 2 lines as shown below


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA"]

// Implement sorting
let sortedAsc = someArray.sorted(by: { (string1: String, string2: String) -> Bool in return string1 < string2 })
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArray.sorted(by: { (string1: String,string2: String) -> Bool in return string1 > string2 })
print("Sorted descending: \(sortedDesc)")




Next, we will try using an array of tuples.


let someArrayOfTupple = [(1,"US"),(44,"UK"),(852,"HK"),(81,"JP"),(82,"KR"), (86,"CN") ]

// Implement sorting
let sortedAsc = someArrayOfTupple.sorted(by:{ (tupple1:(code: Int, name: String), tupple2:(code: Int, name: String)) -> Bool in return tupple1 < tupple2})
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArrayOfTupple.sorted(by: { (tupple1:(code: Int, name: String), tupple2:(code: Int, name: String)) -> Bool in return tupple1 > tupple2} )
print("Sorted descending: \(sortedDesc)")

// Sorting by country name
let sortedAsc2 = someArrayOfTupple.sorted(by:{ (tupple1:(code: Int, name: String), tupple2:(code: Int, name: String)) -> Bool in return tupple1.name < tupple2.name})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someArrayOfTupple.sorted(by: { (tupple1:(code: Int, name: String), tupple2:(code: Int, name: String)) -> Bool in return tupple1.name > tupple2.name} )
print("Sorted descending by name: \(sortedDesc2)")




As you can see from the example above, comparing tuple is the same as comparing the first item. Therefore the two following statement produce the same result


// Implement sorting
let sortedAsc = someArrayOfTupple.sorted(by:{ (tupple1:(code: Int, name: String), tupple2:(code: Int, name: String)) -> Bool in return tupple1 < tupple2})
let sortedAsc1 = someArrayOfTupple.sorted(by:{ (tupple1:(code: Int, name: String), tupple2:(code: Int, name: String)) -> Bool in return tupple1.code < tupple2.code})

print("Sorted ascending: \(sortedAsc)")
print("Sorted ascending: \(sortedAsc1)")




Please note that for dictionary, we cannot put in the standard dictionary type in the parameter; it will generate error:


For dictionary, we must put in parameter similar to tuples as shown below:


let someDictionary = [1:"US", 44:"UK", 852: "HK", 81:"JP", 82: "KR", 86:"CN"]
// Implement sorting
let sortedAsc = someDictionary.sorted(by:{ (dict1:(key: Int, value: String), dict2:(key: Int, value: String)) -> Bool in return dict1 < dict2})
print("Sorted ascending: \(sortedAsc)")

let sortedAsc1 = someDictionary.sorted(by:{ (dict1:(key: Int, value: String), dict2:(key: Int, value: String)) -> Bool in return dict1.key < dict2.key})
print("Sorted ascending: \(sortedAsc1)")

let sortedDesc = someDictionary.sorted(by: { (dict1:(key: Int, value: String), dict2:(key: Int, value: String)) -> Bool in return dict1.key > dict2.key} )
print("Sorted descending: \(sortedDesc)")




The example above is the implementation of key sorting. If we want to sort by value, please refer to the example below:


let someDictionary = [1:"US", 44:"UK", 852: "HK", 81:"JP", 82: "KR", 86:"CN"]

// Sorting by country name
let sortedAsc2 = someDictionary.sorted(by:{ (dict1:(key: Int, value: String), dict2:(key: Int, value: String)) -> Bool in return dict1.value < dict2.value})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someDictionary.sorted(by: { (dict1:(key: Int, value: String), dict2:(key: Int, value: String)) -> Bool in return dict1.value > dict2.value} )
print("Sorted descending by name: \(sortedDesc2)")




Simplification of Closure Expression

As we have mentioned earlier, we are able to simplified the closure expression.  We will be going through each simplification criteria. Please note that the example in the previous closure are written in full expression.


Please also note that although we can simplified the closure expression, it is not encourage to simplify a closure at the expense of maintaining readability for the readers of the code.


Inferring Data Type

Since the closure for the sorted function is passed as argument to a method. Swift can infer its parameter and return data type.


The following example is the simplification of previous example by removing the parameter data type:


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA"]

// Removing data type in parameter
let sortedAsc = someArray.sorted(by: { (string1, string2) -> Bool in return string1 < string2 })
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArray.sorted(by: { (string1,string2) -> Bool in return string1 > string2 })
print("Sorted descending: \(sortedDesc)")




The next example is the same as previous with return data type removed:


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA"]

// Removing data type in parameter
let sortedAsc = someArray.sorted(by: { (string1, string2) in return string1 < string2 })
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArray.sorted(by: { (string1,string2) in return string1 > string2 })
print("Sorted descending: \(sortedDesc)")




We will be going through another example with tuples:


let someArrayOfTupple = [(1,"US"),(44,"UK"),(852,"HK"),(81,"JP"),(82,"KR"), (86,"CN") ]

// Implement sorting
let sortedAsc = someArrayOfTupple.sorted(by:{ (tupple1, tupple2) in return tupple1.0 < tupple2.0})
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArrayOfTupple.sorted(by: { (tupple1, tupple2) in return tupple1.0 > tupple2.0} )
print("Sorted descending: \(sortedDesc)")

// Sorting by country name
let sortedAsc2 = someArrayOfTupple.sorted(by:{ (tupple1, tupple2) in return tupple1.1 < tupple2.1})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someArrayOfTupple.sorted(by: { (tupple1, tupple2) in return tupple1.1 > tupple2.1} )
print("Sorted descending by name: \(sortedDesc2)")




Please note that for the example above since we have removed the parameters, we cannot refer to the tuple element by label. Instead we refer to the tuple element by .0 as first element and so on.


If we have a tuple with many element, then i would suggest the we keep the element. We can just removed the return data type since it is obvious.


The following example is the same as previous by with only return data type remove. This way we still can refer to the element by name:


let someArrayOfTupple = [(1,"US"),(44,"UK"),(852,"HK"),(81,"JP"),(82,"KR"), (86,"CN") ]

// Implement sorting
let sortedAsc = someArrayOfTupple.sorted(by:{ (tupple1:(code: Int, name: String), tupple2:(code: Int, name: String)) in return tupple1.code < tupple2.code})
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArrayOfTupple.sorted(by: { (tupple1:(code: Int, name: String), tupple2:(code: Int, name: String)) in return tupple1.code > tupple2.code} )
print("Sorted descending: \(sortedDesc)")

// Sorting by country name
let sortedAsc2 = someArrayOfTupple.sorted(by:{ (tupple1:(code: Int, name: String), tupple2:(code: Int, name: String)) in return tupple1.name < tupple2.name})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someArrayOfTupple.sorted(by: { (tupple1:(code: Int, name: String), tupple2:(code: Int, name: String)) in return tupple1.name > tupple2.name} )
print("Sorted descending by name: \(sortedDesc2)")


The next example we are looking at dictionary:


let someDictionary = [1:"US", 44:"UK", 852: "HK", 81:"JP", 82: "KR", 86:"CN"]

// Implement sorting
let sortedAsc1 = someDictionary.sorted(by:{ (dict1, dict2) in return dict1.key < dict2.key})
print("Sorted ascending: \(sortedAsc1)")

let sortedDesc = someDictionary.sorted(by: { (dict1, dict2) in return dict1.key > dict2.key} )
print("Sorted descending: \(sortedDesc)")

// Sorting by country name
let sortedAsc2 = someDictionary.sorted(by:{ (dict1, dict2) in return dict1.value < dict2.value})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someDictionary.sorted(by: { (dict1, dict2) in return dict1.value > dict2.value} )
print("Sorted descending by name: \(sortedDesc2)")




Since by default dictionary consist of key and value, so using the parameter is redundant. We can still refer to key and value without the details of parameter.

Implicit Return from Single Expression Closure

First we need to identify what are the single expression. Single expression means a single line return statement. In the sorting example, our expression are "return string1 > string" . This is single line expression.


In Swift Closure, for single line expression, we can omit the return keyword.


The following will be the simplification of our previous example.


Array example:


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA"]

// Removing data type in parameter
let sortedAsc = someArray.sorted(by: { (string1, string2) in string1 < string2 })
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArray.sorted(by: { (string1,string2) in string1 > string2 })
print("Sorted descending: \(sortedDesc)")




Tuple example:


let someArrayOfTupple = [(1,"US"),(44,"UK"),(852,"HK"),(81,"JP"),(82,"KR"), (86,"CN") ]

// Implement sorting
let sortedAsc = someArrayOfTupple.sorted(by:{ (tupple1, tupple2) in tupple1.0 < tupple2.0})
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArrayOfTupple.sorted(by: { (tupple1, tupple2) in tupple1.0 > tupple2.0} )
print("Sorted descending: \(sortedDesc)")

// Sorting by country name
let sortedAsc2 = someArrayOfTupple.sorted(by:{ (tupple1, tupple2) in tupple1.1 < tupple2.1})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someArrayOfTupple.sorted(by: { (tupple1, tupple2) in tupple1.1 > tupple2.1} )
print("Sorted descending by name: \(sortedDesc2)")




Dictionary example:


let someDictionary = [1:"US", 44:"UK", 852: "HK", 81:"JP", 82: "KR", 86:"CN"]
// Implement sorting
let sortedAsc1 = someDictionary.sorted(by:{ (dict1, dict2) in dict1.key < dict2.key})
print("Sorted ascending: \(sortedAsc1)")
let sortedDesc = someDictionary.sorted(by: { (dict1, dict2) in dict1.key > dict2.key} )
print("Sorted descending: \(sortedDesc)")

// Sorting by country name
let sortedAsc2 = someDictionary.sorted(by:{ (dict1, dict2) in dict1.value < dict2.value})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someDictionary.sorted(by: { (dict1, dict2) in dict1.value > dict2.value} )
print("Sorted descending by name: \(sortedDesc2)")




Shorthand Argument Names

Swift automatically provide shorthand argument to inline closure. $0 refers to first argument, $1 refers to second argument and so on. When we use shorthand argument, declaration is not necessary for the parameter and we can omit the in keyword.


Array example:


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA"]

// Removing data type in parameter
let sortedAsc = someArray.sorted(by: { $0 < $1 })
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArray.sorted(by: { $0 > $1 })
print("Sorted descending: \(sortedDesc)")




Tuple example:


let someArrayOfTupple = [(1,"US"),(44,"UK"),(852,"HK"),(81,"JP"),(82,"KR"), (86,"CN") ]

// Implement sorting
let sortedAsc = someArrayOfTupple.sorted(by:{ $0.0 < $1.0})
print("Sorted ascending: \(sortedAsc)")
let sortedDesc = someArrayOfTupple.sorted(by: { $0.0 > $1.0} )
print("Sorted descending: \(sortedDesc)")

// Sorting by country name
let sortedAsc2 = someArrayOfTupple.sorted(by:{ $0.1 < $1.1})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someArrayOfTupple.sorted(by: { $0.1 > $1.1} )
print("Sorted descending by name: \(sortedDesc2)")




Dictionary example:


let someDictionary = [1:"US", 44:"UK", 852: "HK", 81:"JP", 82: "KR", 86:"CN"]

// Implement sorting
let sortedAsc1 = someDictionary.sorted(by:{ $0.key < $1.key})
print("Sorted ascending: \(sortedAsc1)")
let sortedDesc = someDictionary.sorted(by: { $0.key > $1.key} )
print("Sorted descending: \(sortedDesc)")

// Sorting by country name
let sortedAsc2 = someDictionary.sorted(by:{ $0.value < $1.value})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someDictionary.sorted(by: { $0.value > $1.value} )
print("Sorted descending by name: \(sortedDesc2)")




Operator Method

There are more simpler way. The string data type also specified a method using a operator for comparison. It has 2 string as parameter and 1 boolean as return. Therefore, we can just utilised this method in the array of string.


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA"]

// Removing data type in parameter
let sortedAsc = someArray.sorted(by: <)
print("Sorted ascending: \(sortedAsc)")

let sortedDesc = someArray.sorted(by: >)
print("Sorted descending: \(sortedDesc)")




For tuple, there is also a method that allows an operator for comparison. However, this only apply to the first element. There is no way to simplified if we want to use other element.


let someArrayOfTupple = [(1,"US"),(44,"UK"),(852,"HK"),(81,"JP"),(82,"KR"), (86,"CN") ]

// Implement sorting
let sortedAsc = someArrayOfTupple.sorted(by:<)
print("Sorted ascending: \(sortedAsc)")
let sortedDesc = someArrayOfTupple.sorted(by:>)
print("Sorted descending: \(sortedDesc)")

// Sorting by country name
let sortedAsc2 = someArrayOfTupple.sorted(by:{$0.1 < $1.1})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someArrayOfTupple.sorted(by: {$0.1 > $1.1} )
print("Sorted descending by name: \(sortedDesc2)")




Similarity, for dictionary, Swift also allow an operator for comparison. This operator method only applies to the key. If we want to sort the value, we must use the shorthand argument.


let someDictionary = [1:"US", 44:"UK", 852: "HK", 81:"JP", 82: "KR", 86:"CN"]

// Implement sorting
let sortedAsc1 = someDictionary.sorted(by:<)
print("Sorted ascending: \(sortedAsc1)")
let sortedDesc = someDictionary.sorted(by:>)
print("Sorted descending: \(sortedDesc)")

// Sorting by country name
let sortedAsc2 = someDictionary.sorted(by:{$0.value < $1.value})
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someDictionary.sorted(by: {$0.value > $1.value} )
print("Sorted descending by name: \(sortedDesc2)")




Trailing Closure

If a closure is too long, we can use trailing closure. Normally, the closure we use is called as follows:
<function_that_take_closure>.(<clsoure_parameter>:{ ( <parameters> ) -> <return type> in
<set_of_instructions>
})


If the closure is too long, we can use the trailing closure as follows:
<function_that_take_closure>.(<clsoure_parameter>:) { ( <parameters> ) -> <return type> in
<set_of_instructions>
}


If the close is the only argument, we can ignore the brackets as shown below
<function_that_take_closure> { ( <parameters> ) -> <return type> in
<set_of_instructions>
}


Using the sorted method method with un-simplified expression, we can write trailing closure as follows:


let someDictionary = [1:"US", 44:"UK", 852: "HK", 81:"JP", 82: "KR", 86:"CN"]

// Sorting by country name
let sortedAsc2 = someDictionary.sorted(by:) {
   (dict1:(key: Int, value: String), dict2:(key: Int, value: String)) -> Bool in
   return dict1.value < dict2.value
}
print("Sorted ascending by name: \(sortedAsc2)")
let sortedDesc2 = someDictionary.sorted {
   (dict1:(key: Int, value: String), dict2:(key: Int, value: String)) -> Bool in
   return dict1.value > dict2.value
}
print("Sorted descending by name: \(sortedDesc2)")




In Array, we have a mapping function were we can dictate how the array element was to be mapped in a closure. We will be using this function to perform a trailing closure.


Example:


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA", "NA"]
let someDictionary = ["US":1,"UK":44,"HK":852,"FR":33,"DE":49,"JP":81,"KR":82,"CN":86, "CA":1]

let arrayInCode = someArray.map { (country) -> Int in

   if let foundInDictionary = someDictionary[country] {
       return foundInDictionary
   } else {
       return 0
   }
}

print(arrayInCode)




In the example above, we suppose to create an alternative array that map to the respective country code base on the dictionary provide. Those country code that are not found in the dictionary, the closure return a 0 instead. This closure is written in trailing closure format.


Application of Closure

If we have work on array or dictionary before, we know that arrays and dictionary requires closure in some of the function. This is the most common application of closure.  We can also create function that takes in closure.


Listed below, we will demonstrate different application of closure.


Sorted(by:)

Sorting array example:


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA", "NA"]

let sortedArray = someArray.sorted(by: <)
print(sortedArray)




Contains()

We can formulate our implementation to find if an item we are looking for is in the array.


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA"]

let containsAsiaCountry = someArray.contains { (country) in
   switch country {
   case "CN", "KR", "JP", "HK":
       return true
   default:
       return false
   }
}

print(containsAsiaCountry)




The closure above just confirm if there is any asian countries in the array.


Index(where:)

The following array return the index with the condition that takes in closure.


let someArray = ["US", "UK", "HK", "FR", "DE", "JP", "KR", "CN", "CA"]

// This closure find the index of the element that is equal to DE
if let germanyIndex = someArray.index(where:{$0 == "DE"}) {
   print(someArray[germanyIndex])
}




Drop(while:)

This closure is not so straight forward. It will check first element with the condition in the closure. If it is false, it will stop running the closure, if it is true it will run the closure for each element until it is false or end of array.


We cannot find condition that fits the closure requirement. We will use filter function instead.


Filter()

Instead of using drop while method, we use filter method with closure.


let someArray = ["US", "UK", "HK", "FR", "DE", "NA", "JP", "KR", "NA", "CN", "CA"]

let cleanArray = someArray.filter({$0 != "NA"})
print(cleanArray)




let someWords = ["biology","cosmology","anthropology", "epidemiology", "psychology", "microbiology","pharmacology", "epistemology", "anthropology", "physiology", "biotechnology", "biometeorology"]
let lessList =  someWords.filter({$0.hasPrefix("bio")})
print(lessList)




Iterate Array using forEach()

Instead of using for in loop, we can use forEach method to iterate over the each item in an array.


let someArray = ["US", "UK", "HK", "FR", "DE", "NA", "JP", "KR", "CN"]
someArray.forEach({print("The country code is \($0)")})




Using forEach method can be for concise then for-in loop but the effect is the same. See the similar for-in loop.


let someArray = ["US", "UK", "HK", "FR", "DE", "NA", "JP", "KR", "CN"]
for item in someArray {
   print("The country code is \(item)")
}




We can also use forEach for dictionary as shown below:


var countryCode = [65:"SG",1:"US",44:"UK",852:"HK",86:"CN"]
countryCode.forEach({print("The country code for \($0.value) is \($0.key)") })




Map()

We can use map method to return any type of mapping we want.


let someArray = ["US", "UK", "HK", "FR", "DE", "NA", "JP", "KR", "CN"]
let someMap = ["US":1, "UK":44, "HK":852, "FR":33, "DE":49, "JP":81, "KR":82, "CN":86]
let tempCountryCode = someArray.map{ (myArray) -> Int in
   if let indexCode = someMap.index(forKey: myArray) {
       let code = someMap[indexCode].value
       print(code)
       return code
   } else {
       return 0
   }
}

print(tempCountryCode)
let finalCountryCode = tempCountryCode.filter({$0 != 0})
print(finalCountryCode)




We can also use map to perform computation such as squaring each number:
var numberList = [12,34,54,67,43,89,23,38,72]
let squareList = numberList.map {$0 * $0}
print(squareList)




It is best for conversion as shown below:


let temperateInCelsius = [0.0, 100.0, 36.9, 42.0, 26.0]
let convertedTempInFahrenheit = temperateInCelsius.map { $0 * (9/5) + 32 }
print(convertedTempInFahrenheit)




CompactMap()

We can use compactMap() method to remove any nil item in an optional array.


let userFeedbackName = ["Alvin", nil, "Timothy", nil, "Vincent"]
let compactNameList = userFeedbackName.compactMap({$0})
print(compactNameList)




Reduce()

The reduce function allow us to reduce the array list to a single item such as cumulative add.


var numberList = [12,34,54,67,43,89,23,38,72]
let total = numberList.reduce(0) {$0 + $1}
print("Total = \(total)")




Chaining Closure

We can chain a number of closure as follows:


Suppose we have a list of student score. First we would like to filter out the failures. Then we map into sore from 1 to 5 and finally we reduce it into average score.


/*
We have a list of student score of 5 papers in percentage
First we filter out the faulre with passing grade of 250
Next we convert to score of 1 to 5 using map
finally we reduce into total and take the average
*/
let studentScore:[Double] = [487, 201, 165, 387, 236, 280, 365, 375, 412, 328]
let newList = studentScore.filter({$0 >= 250.0})
let gpa = (newList.map({$0/100}).reduce(0.0){$0 + $1})/Double(newList.count)
print("For student who pass the average gpa is \(gpa)")




Capturing Values

A closure can capture variables from the surrounding context. It can modified the value of these variables in the closure body.


The closest form of capture is a nested function. A nested function can capture any variables define outside its function. All nested function can capture variables outside its defined function, even from another file but within the same project.

This subject will be explored further.

***

No comments:

Post a Comment