스위프트는 함수를 일급 객체로 취급한다. 따라서 함수를 다른 함수의 전달인자로 사용할 수 있다. 매개변수로 함수를 갖는 함수를 고차함수라고 부르는데, 스위프트에 유용한 대표적인 고차함수로는 맵, 필터, 리듀스 등이 있다.
15.1 맵(Map)
맵은 자신을 호출할 때 매개변수로 전달된 함수를 실행하여 그 결과를 다시 반환해주는 함수이다. 스위프트에서 맵은 배열, 딕셔너리, 세트, 옵셔널 등에서 사용할 수 있다. 더 정확히 말하자면 스위프트의 Sequence, Collection 프로토콜을 따르는 타입과 옵셔널은 모두 맵을 사용할 수 있다.
맵을 사용하면 컨테이너가 담고 있던 각각의 값을 매개변수를 통해 받은 함수에 적용한 후 다시 컨테이너에 포장하여 반환한다. 기존 컨테이너의 값은 변경되지 않고 새로운 컨테이너가 생성되어 반환된다. 그래서 맵은 기존 데이터를 변형하는 데 많이 사용한다.
map 메서드의 사용법은 for-in 구문과 별반 차이가 없지만 코드의 재사용 측면이나 컴파일러 최적화 측면에서 본다면 성능 차이가 있다. 또, 다중 스레드 환경일 때 대상 컨테이너의 값이 스레드에서 변경되는 시점에 다른 스레드에서도 동시에 값이 변경되려고 할 때 예측하지 못한 결과가 발생하는 부작용을 방지할 수도 있다.
/*
코드 15-1. for-in 구문과 맵 메서드의 사용 비교
*/
let numbers: [Int] = [0, 1, 2, 3, 4]
var doubledNumbers: [Int] = [Int]()
var strings: [String] = [String]()
// for 구문 사용
for number in numbers {
doubledNumbers.append(number * 2)
strings.append("\(number)")
}
print(doubledNumbers) // [0, 2, 4, 6, 8]
print(strings) // ["0", "1", "2", "3", "4"]
// map 메서드 사용
doubledNumbers = numbers.map({ (number: Int) -> Int in
return number * 2
})
strings = numbers.map({ (number: Int) -> String in
return "\(number)"
})
print(doubledNumbers) // [0, 2, 4, 6, 8]
print(strings) // ["0", "1", "2", "3", "4"]
map 메서드를 사용하면 for-in 구문을 사용하기 위하여 빈 배열을 처음 생성해주는 작업이 필요 없다. 배열의 append 연산을 실행하기 위한 시간도 필요 없다.
클로저 표현식을 사용하여 map에서의 표현을 더 간략화해볼 수 있다.
/*
코드 15-2. 클로저 표현의 간략화
*/
let numbers: [Int] = [0, 1, 2, 3, 4]
// 기본 클로저 표현식 사용
var doubledNumbers = numbers.map({ (number: Int) -> Int in
return number * 2
})
// 매개변수 및 반환 타입 생략
doubledNumbers = numbers.map( {return $0 * 2} )
print(doubledNumbers) // [0, 2, 4, 6, 8]
// 반환 키워드 생략
doubledNumbers = numbers.map( { $0 * 2 })
print(doubledNumbers)
// 후행 클로저 사용
doubledNumbers = numbers.map { $0 * 2 }
print(doubledNumbers)
같은 기능을 여러 번 사용할 것이라면 하나의 클로저를 여러 map 메서드에서 사용하는 편이 좋을 것 같다. [코드 15-3]을 통해 재사용 가능한 코드로 재구성했다.
/*
코드 15-3. 클로저의 반복 사용
*/
let evenNumbers: [Int] = [0, 2, 4, 6, 8]
let oddNumbers: [Int] = [0, 1, 3, 5, 7]
let multiplyTwo: (Int) -> Int = { $0 * 2 }
let doubledEvenNumbers = evenNumbers.map(multiplyTwo)
print(doubledEvenNumbers) // [0, 4, 8, 12, 16]
let doubledOddNumbers = oddNumbers.map(multiplyTwo)
print(doubledOddNumbers) // [0, 2, 6, 10, 14]
map 메서드는 배열뿐만 아니라 여러 컨테이너 타입에 모두 적용이 가능하다. [코드 15-4]는 다양한 종류의 컨테이너에서 map 메서드를 실행해본 결과이다.
/*
코드 15-4. 다양한 컨테이너 타입에서의 맵의 활용
*/
let alphabetDictionary: [String: String] = ["a": "A", "b": "B"]
var keys: [String] = alphabetDictionary.map { (tuple: (String, String)) -> String in
return tuple.0
}
keys = alphabetDictionary.map{ $0.0 }
let values: [String] = alphabetDictionary.map{ $0.1 }
print(keys) // ["b", "a"]
print(values) // ["B", "A"]
var numberSet: Set<Int> = [1, 2, 3, 4, 5]
let resultSet = numberSet.map{ $0 * 2 }
print(resultSet) // [2, 4, 6, 8, 10]
let optionalInt: Int? = 3
let resultInt: Int? = optionalInt.map{ $0 * 2 }
print(resultInt) // 6 - 경고가 발생하는 이유는 타입캐스팁의 tip에서 설명한다.
let range: CountableClosedRange = (0...3)
let resultRange: [Int] = range.map{ $0 * 2 }
print(resultRange) // [0, 2, 4, 6]
15.2 필터
필터는 말 그대로 컨테이너 내부의 값을 걸러서 추출하는 역할을 하는 고차함수이다. 맵과 마찬가지로 새로운 컨테이너에 값을 담아 반환해준다. 다만 맵처럼 기존 콘텐츠를 변형하는 것이 아니라, 특정 조건에 맞게 걸러내는 역할을 할 수 있다는 점이 다르다.
filter 함수의 매개변수로 전달되는 함수의 반환 타입은 Bool이다. 해당 콘텐츠의 값을 갖고 새로운 컨테이너에 포함될 항목이라도 판단하면 true를, 포함하지 않으려면 false를 반환해주면 된다.
/*
코드 15-5. 필터 메서드의 사용
*/
let numbers: [Int] = [0, 1, 2, 3, 4, 5]
let evenNumbers: [Int] = numbers.filter { (number: Int) -> Bool in
return number % 2 == 0
}
print(evenNumbers) // [0, 2, 4]
let oddNumbers: [Int] = numbers.filter { $0 % 2 == 1}
print(oddNumbers) // [1, 3, 5]
만약 콘텐츠의 변형 후에 필터링하고 싶다면 [코드 15-6]처럼 맵을 사용한 후에 필터 메서드를 호출하면 된다.
/*
코드 15-6. 맵과 필터 메서드의 연계 사용
*/
let numbers: [Int] = [0, 1, 2, 3, 4, 5]
let mappedNumbers: [Int] = numbers.map{ $0 + 3 }
let evenNumbers: [Int] = mappedNumbers.filter{ (number: Int) -> Bool in
return number % 2 == 0
}
print(evenNumbers) // [4, 6, 8]
// mappedNumbers를 굳이 여러 번 사용할 필요가 없다면 메서드를 체인처럼 연결하여 사용할 수 있다.
let oddNumbers: [Int] = numbers.map{ $0 + 3 }.filter{ $0 % 2 == 1 }
print(oddNumbers) // [3, 5, 7]
15.3 리듀스(Reduce)
리듀스 기능은 사실 결합이라고 불려야 마땅한 기능이다. 리듀스는 컨테이너 내부의 콘텐츠를 하나로 합하는 기능을 실행하는 고차함수다. 배열이라면 배열의 모든 값을 전달인자로 전달받은 클로저의 연산 결과로 합해준다.
스위프트의 리듀스는 두 가지 형태로 구현되어 있다. 첫 번째 리듀스는 클로저가 각 요소를 전달받아 연산한 후 값을 다음 클로저 실행을 위해 반환하며 컨테이너를 순환하는 형태이다.
public func reduce<Result>(_ initialResult: Result,
_nextPartialResult: (Result, Element) throws -> Result)
rethrows -> Result
initialResult라는 이름의 매개변수로 전달되는 값을 통해 초깃값을 지정해 줄 수 있다. nextPartialResult라는 이름의 매개변수로 클로저를 전달받는다. nextPartialResult 클로저의 첫 번째 매개변수는 리듀스 메서드의 initialResult 매개변수를 통해 전달받은 초깃값 또는 이전 클로저의 결괏값이다. 모든 순회가 끝나면 리듀스의 최종 결괏값이 된다. 두 번째 매개변수는 리듀스 메서드가 순환하는 컨테이너의 요소이다.
두 번째 리듀스 메서드는 컨테이너를 순환하며 클로저가 실행되지만 클로저가 따로 결괏값을 반환하지 않는 형태이다. 대신 inout 매개변수를 사용하여 초깃값에 직접 연산을 실행하게 된다.
public func reduce<Result>(into initialResult: Result,
_ updateAccumulatingResult: (inout Result, Element) throws -> ())
rethrows -> Result
updateAccumulatingResult 매개변수로 전달받는 클로저의 매개변수 중 첫 번째 매개변수를 inout 매개변수로 사용한다. updateAccumulatingResult 클로저의 첫 번째 매개변수는 리듀스 메서드의 initialResult 매개변수를 이용해 전달받은 초깃값 또는 이전에 실행된 클로저 때문에 변경되어 있는 결괏값이다. 모든 순회가 끝나면 리듀스의 최종 결괏값이 된다. 두 번째 매개변수는 리듀스 메서드가 순환하는 컨테이너의 요소이다. 상황에 따라서는 리듀스를 맵과 유사하게 사용할 수도 있다.
/*
코드 15-7. 리듀스 메서드의 사용
*/
let numbers: [Int] = [1, 2, 3]
// 첫 번째 형태인 reduce(_:_:) 메서드의 사용
// 초깃값이 0이고 정수 배열의 모든 값을 더한다.
var sum: Int = numbers.reduce(0, { (result: Int, next: Int) -> Int in
print("\(result) + \(next)")
// 0 + 1
// 1 + 2
// 3 + 3
return result + next
})
print(sum) // 6
// 초깃값이 0이고 정수 배열의 모든 값을 뺀다.
let subtract: Int = numbers.reduce(0, { (result: Int, next: Int) -> Int in
print("\(result) - \(next)")
// 0 - 1
// -1 - 2
// -3 - 3
return result - next
})
print(subtract) // -6
// 초깃값이 3이고 정수 배열의 모든 값을 더한다.
let sumFromThree: Int = numbers.reduce(3) {
print("\($0) + \($1)")
// 3 + 1
// 4 + 2
// 6 + 3
return $0 + $1
}
print(sumFromThree) // 9
// 초깃값이 3이고 정수 배열의 모든 값을 뺀다.
var subtractFromThree: Int = numbers.reduce(3) {
print("\($0) - \($1)")
// 3 - 1
// 2 - 2
// 0 - 3
return $0 - $1
}
print(subtractFromThree) // -3
// 문자열 배열을 reduce(_:_:) 메서드를 이용해 연결시킨다
let names: [String] = ["Chope", "Jay", "Joker", "Nova"]
let reducedNames: String = names.reduce("yagom's friend : ") {
return $0 + ", " + $1
}
print(reducedNames) // yagom's friend : , Chope, Jay, Joker, Nova
// 두 번째 형태인 reduce(into:_:) 메서드의 사용
// 초깃값이 0이고 정수 배열의 모든 값을 더한다.
// 첫 번째 리듀스 형태와 달리 클로저의 값을 반환하지 않고 내부에서
// 직접 이전 값을 변경한다는 점이 다르다.
sum = numbers.reduce(into: 0, { (result: inout Int, next: Int) in
print("\(result) + \(next)")
// 0 + 1
// 1 + 2
// 3 + 3
result += next
})
print(sum) // 6
// 초깃값이 3이고 정수 배열의 모든 값을 뺀다.
// 첫 번째 리듀스 형태와 달리 클로저의 값을 반환하지 않고 내부에서
// 직접 이전 값을 변경한다는 점이 다르다.
subtractFromThree = numbers.reduce(into: 3, {
print("\($0) - \($1)")
// 3 - 1
// 2 - 2
// 0 - 3
$0 -= $1
})
print(subtractFromThree) // -3
// 첫 번째 리듀스 형태와 다르기 때문에 다른 컨테이너에 값을 변경하여 넣어줄 수도 있다.
// 이렇게 하면 맵이나 필터와 유사한 형태로 사용할 수도 있다.
// 홀수는 걸러내고 짝수만 두 배로 변경하여 초깃값인 [1, 2, 3] 배열에 직접 연산한다.
var doubledNumbers: [Int] = numbers.reduce(into: [1, 2]) { (result: inout [Int], next: Int) in
print("result: \(result) next : \(next)")
// result: [1, 2] next : 1
// result: [1, 2] next : 2
// result: [1, 2, 4] next : 3
guard next.isMultiple(of: 2) else {
return
}
print("\(result) append \(next)")
// [1, 2] append 2
result.append(next * 2)
}
print(doubledNumbers) // [1, 2, 4]
// 필터와 맵을 사용한 모습
doubledNumbers = [1, 2] + numbers.filter{ $0.isMultiple(of: 2) }.map{ $0 * 2 }
print(doubledNumbers) // [1, 2, 4]
// 이름을 모두 대문자로 변환하여 초깃값인 빈 배열에 직접 연산
var upperCasedNames: [String]
upperCasedNames = names.reduce(into: [], {
$0.append($1.uppercased())
})
print(upperCasedNames) // ["CHOPE", "JAY", "JOKER", "NOVA"]
// 맵을 사용한 모습
upperCasedNames = names.map { $0.uppercased() }
print(upperCasedNames) // ["CHOPE", "JAY", "JOKER", "NOVA"]
리듀스도 [코드 15-8]처럼 맵과 필터로 결합을 이룰 수 있다.
/*
코드 15-8. 맵, 필터, 리듀스 메서드의 연계 사용
*/
let numbers: [Int] = [1, 2, 3, 4, 5, 6, 7]
// 짝수를 걸러내어 각 값에 3을 곱해준 후 모든 값을 더한다.
var result: Int = numbers.filter{ $0.isMultiple(of: 2) }.map{ $0 * 3 }.reduce(0) {
$0 + $1
}
print(result) // 36
// for-in 구문 사용 시
result = 0
for number in numbers {
guard number.isMultiple(of: 2) else {
continue
}
result += number * 3
}
print(result) // 36
15.4 맵, 필터, 리듀스의 활용
[코드 15-9]와 [코드 15-10]은 목록의 친구들을 특정 조건으로 분류하여 콘솔에 출력하는 예제이다. 맵, 필터, 리듀스를 활용해보자.
/*
코드 15-9. 친구들의 정보 생성
*/
enum Gender {
case male, female, unknown
}
struct Friend {
let name: String
let gender: Gender
let location: String
var age: UInt
}
var friends: [Friend] = [Friend]()
friends.append(Friend(name: "Yoobato", gender: .male, location: "발리", age: 26))
friends.append(Friend(name: "JiSoo", gender: .male, location: "시드니", age: 24))
friends.append(Friend(name: "JuHyun", gender: .male, location: "경기", age: 30))
friends.append(Friend(name: "JiYoung", gender: .female, location: "서울", age: 22))
friends.append(Friend(name: "SungHo", gender: .male, location: "충북", age: 20))
friends.append(Friend(name: "JungKi", gender: .unknown, location: "대전", age: 29))
friends.append(Friend(name: "YoungMin", gender: .male, location: "경기", age: 24))
[코드 15-9]에 입력된 자료는 작년 자료다. 친구들의 나이는 실제 나이보다 한 살 더 적게 적혀 있고 이를 전제로 조건에 맞는 친구를 찾아보자. 조건은 "서울 외의 지역에 거주하며 25세 이상인 친구"이다.
/*
코드 15-10. 조건에 맞는 친구 결과 출력
*/
enum Gender {
case male, female, unknown
}
struct Friend {
let name: String
let gender: Gender
let location: String
var age: UInt
}
var friends: [Friend] = [Friend]()
friends.append(Friend(name: "Yoobato", gender: .male, location: "발리", age: 26))
friends.append(Friend(name: "JiSoo", gender: .male, location: "시드니", age: 24))
friends.append(Friend(name: "JuHyun", gender: .male, location: "경기", age: 30))
friends.append(Friend(name: "JiYoung", gender: .female, location: "서울", age: 22))
friends.append(Friend(name: "SungHo", gender: .male, location: "충북", age: 20))
friends.append(Friend(name: "JungKi", gender: .unknown, location: "대전", age: 29))
friends.append(Friend(name: "YoungMin", gender: .male, location: "경기", age: 24))
var result: [Friend] = friends.map{
Friend(name: $0.name, gender: $0.gender, location: $0.location, age: $0.age + 1)
}
result = result.filter { $0.age >= 25 && $0.location != "서울" }
let string: String = result.reduce("서울 외의 지역에 거주하며 25세 이상인 친구") {
$0 + "\n" + "\($1.name) \($1.gender) \($1.location) \($1.age)세"
}
print(string)
'iOS > SWIFT' 카테고리의 다른 글
[Swift] Chapter 17. 서브스크립트 (0) | 2021.10.10 |
---|---|
[Swift] Chapter 16. 모나드 (0) | 2021.10.10 |
[Swift] Chapter 14. 옵셔널 체이닝과 빠른 종료 (0) | 2021.10.09 |
[Swift] Chapter 13. 클로저 (0) | 2021.10.09 |
[Swift] Chapter 12. 접근제어 (0) | 2021.10.08 |
댓글