Swift 18. 고차함수 (맵, 필터, 리듀스)
Swift 18. 고차함수 (맵, 필터, 리듀스)
Swift는 함수를 일급 객체로 취급 -> 함수를 다른 함수의 전달인자로 사용할 수 있음
고차함수 => 함수를 매개변수로 갖는 함수
(map, filter, reduce 등)
1. map
자신을 호출할 때 매개변수로 전달된 함수를 실행해서 그 결과를 다시 반환해주는 고차함수
배열, 딕셔너리, 세트, 옵셔널 등에서 사용할 수 있음 -> Swift의 Sequence, Collection 프로토콜을 따르는 타입과 옵셔널에서 map을 사용할 수 있음
컨테이너가 담고 있던 각각의 값을 매개변수를 통해 받은 함수에 적용한 후 다시 컨테이너에 포장해서 반환
기존 컨테이너는 변경되지 않고 새로운 컨테이너가 생성되어 반환
=> 기존 데이터를 변형할 때 map을 많이 사용
map은 for-in 문과 사용법이 비슷
for-in 문과 비교했을 때 코드의 재사용 측면이나 컴팡리러 최척화 측면에서 성능의 차이가 있음
다중 스레드 환경일 때 대상 컨테이너의 값이 스레드에 의해 변경되는 시점에 다른 스레드에 의해 동시에 값이 변경되는 부작용을 방지할 수 있음
[map을 적용할 컬렉션].map( [각 요소를 적용할 함수] )
* 예시 - for-in 문과 map 비교
let numbers: [Int] = [0, 1, 2, 3, 4, 5, 6]
// for - in
// 새로운 배열 미리 준비
var doubleNumbers: [Int] = [] // 각 요소애 * 2
var strings: [String] = [] // 각 요소를 문자열로 변환
for i in numbers {
doubleNumbers.append(i * 2)
strings.append("\(i)")
}
print(doubleNumbers)
print(strings)
// map 메소드
// 각 요소에 *2 한 새로운 배열 생성
doubleNumbers = numbers.map( { (num: Int) -> Int in
return num * 2
})
// 각 요소를 문자열로 변환한 새로운 배열 생성
strings = numbers.map( { (num: Int) -> String in
return "\(num)"
})
print(doubleNumbers)
print(strings)
<실행결과>
[0, 2, 4, 6, 8, 10, 12]
["0", "1", "2", "3", "4", "5", "6"]
[0, 2, 4, 6, 8, 10, 12]
["0", "1", "2", "3", "4", "5", "6"]
-> map 메소드를 이용하는 것이 for-in문을 이용한 것 보다 간결하고 편리하게 각 요소의 연산을 수행할 수 있음
(빈 배열을 생성하거나 배열에 append 연산을 실행할 시간도 필요 없음)
* 예시 - 클로저 표현식을 이용해 표현 간략화
let numbers: [Int] = [0, 1, 2, 3, 4, 5, 6]
// 기본 클로저
var doubleNumbers: [Int] = numbers.map( { (num: Int) -> Int in
return num * 2
})
// 매개변수 및 반환 타입 생략
doubleNumbers = numbers.map( {
return $0 * 2
})
// 반환 키워드 생략
doubleNumbers = numbers.map( {
$0 * 2
})
// 후행 클로저 사용
doubleNumbers = numbers.map() {$0 * 2}
print(doubleNumbers)
<실행결과>
[0, 2, 4, 6, 8, 10, 12]
* 예시 - 클로저를 상수에 할당해서 map 메소드에 전달
let numbers1: [Int] = [0, 1, 2, 3]
let numbers2: [Int] = [4, 5, 6, 7]
// 클로저를 상수에 할당
let doubleNum: (Int) -> Int = {$0 * 2}
// map 메소드에 클로저 상수 전달
var doubleNumbers1: [Int] = numbers1.map(doubleNum)
var doubleNumbers2: [Int] = numbers2.map(doubleNum)
print(doubleNumbers1)
print(doubleNumbers2)
<실행결과>
[0, 2, 4, 6]
[8, 10, 12, 14]
* 예시 - 다양한 컨테이너에서의 map 메소드 활용
// 딕셔너리에 map 메소드 적용
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)
print(values)
// 세트에 map 메소드 적용
var numberSet: Set<Int> = [1, 2, 3, 4, 5]
let resultSet = numberSet.map{ $0*2 }
print(resultSet)
// 옵셔널에 map 메소드 적용
let optionalInt: Int? = 3
let resultInt: Int? = optionalInt.map{ $0*2 }
print(resultInt)
// CountableClosedRange 타입에 map 메소드 적용 (연속된 값의 범위 타입)
let range: CountableClosedRange = (0...3)
let resultRange: [Int] = range.map{ $0*2}
print(resultRange)
<실행결과>
["b", "a"]
["B", "A"]
[6, 8, 10, 4, 2]
Optional(6)
[0, 2, 4, 6]
2. filter
컨테이너 내부의 값을 걸러서 추출하는 고차함수
map과 마찬가지로 새로운 컨테이너에 값을 담아 반환
기존 콘텐츠를 변형하는 것이 아닌 특정 조건에 맞게 걸러내는 역할
filter 함수에 매개변수로 전달되는 함수의 반환 타입은 반드시 Bool 이어야 함
컨테이너의 각 요소가 새로운 컨테이너에 포함될 조건에 부합하면 true, 아니면 false 반환
[적용할 컨테이너].filter( [적용할 함수 (반환타입 Bool)] )
* 예시
let numbers: [Int] = [0, 1, 2, 3, 4, 5]
// 짝수만 추출
let evenNums: [Int] = numbers.filter( { (num: Int) -> Bool in
return num % 2 == 0
})
print(evenNums)
// 홀수만 추출
let oddNums: [Int] = numbers.filter( { (num: Int) -> Bool in
return num % 2 != 0
})
print(oddNums)
<실행결과>
[0, 2, 4]
[1, 3, 5]
- 각 요소를 변형한 후 필터링 하고 싶다면 map을 적용하고 filter를 적용하면 됨
메소드를 체인처럼 연결해서 작성해도 됨
( ex - [컨테이너].map().filter() )
* 예시 - 각 요소에 3을 더한 후 짝수, 홀수를 걸러내기
let numbers: [Int] = [0, 1, 2, 3, 4, 5]
// + 3
let mappedNums: [Int] = numbers.map() { $0 + 3 }
// 짝수만 추출
let evenNums: [Int] = mappedNums.filter( { (num: Int) -> Bool in
return num % 2 == 0
})
print(evenNums)
// 메소드를 체인처럼 연결해서 구현해도 됨
// 홀수만 추출
let oddNums: [Int] = numbers.map(){$0 + 3}.filter(){$0 % 2 != 0}
print(oddNums)
<실행결과>
[4, 6, 8]
[3, 5, 7]
3. reduce
컨테이너의 각 요소들을 하나로 합치는 고차함수 -> 결합
컨테이너의 모든 요소들을 전달인자로 전달받은 클로저에 적용해서 함해줌
reduce의 두가지 형태
(1) 값 전달 - reduce(_:, _:)
클로저가 각 요소를 전달받아 연산한후 값을 반환하면 다음 클로저에 전달되어 계속해서 연산 결과를 누적하며 컨테이너 순환
첫번째 매개변수에는 누적할 초기값을 지정
두번째 매개변수에는 연산을 수행할 클로저 지정
클로저의 첫번째 매개변수에는 초기값이 전달되고 이후에는 이전 클로저의 반환값이 전달됨
클로저의 두번째 매개변수에는 컨테이너의 각 요소가 전달됨
(2) 참조 전달 - reduce(into:, _:)
클로저가 각 요소를 전달받아 연산한 후 결과값을 반환하지 않고 inout매개변수로 초기값의 참조를 전달해서 연산결과를 누적하며 컨테이너 순환
첫번째 매개변수에는 누적할 초기값을 지정
두번째 매개변수에는 연산을 수행할 클로저 지정
클로저의 첫번째 매개변수에는 초기값의 참조가 전달되고 이후에는 초기값이 변경되서 참조로 전달됨 -> 반드시 inout 키워드를 매개변수의 자료형 앞에 작성해서 참조가 전달되도록 지정
클로저의 두번째 매개변수에는 컨테이너의 각 요소가 전달됨
reduce를 map와 유사하게 사용할 수 있게 해줌
* 예시 - reduce(_:, _:)
let numbers: [Int] = [1, 2, 3]
// 초깃값이 0이고 정수 배열의 모든 값을 더함
var sum: Int = numbers.reduce(0, { (result: Int, next: Int) -> Int in
return result + next
})
print(sum)
// 초깃값이 3이고 정수 배열의 모든 값을 뺌
var subtract: Int = numbers.reduce(3) {
return $0 - $1
}
print(subtract)
// 문자열을 모두 연결
let names: [String] = ["Kim", "Lee", "Park"]
let reducedNames: String = names.reduce("my friends : ") {$0 + ", " + $1}
print(reducedNames)
<실행결과>
6
-3
my friends : , Kim, Lee, Park
* 예시 - reduce(into:, _)
let numbers: [Int] = [1, 2, 3]
// 초깃값이 0이고 정수 배열의 모든 값을 더함
var sum: Int = numbers.reduce(into: 0, { (result: inout Int, next: Int) in
result += next
})
print(sum)
// 초깃값이 3이고 정수 배열의 모든 값을 뺌
var subtract: Int = numbers.reduce(into: 3) {
$0 -= $1
}
print(subtract)
// 문자열을 모두 연결
let names: [String] = ["Kim", "Lee", "Park"]
let reducedNames: String = names.reduce(into : "my friends : ") {$0 += (", " + $1)}
print(reducedNames)
<실행결과>
6
-3
my friends : , Kim, Lee, Park
- reduce의 두번째 유형은 참조를 전달받기 때문에 다른 컨테이너에 값을 변경해서 넣어줄 수 있음. 그러면 map이나 filterd와 유사한 영태로 사용할 수 있음
* 예시
// 짝수만 걸러내서 * 2 한 후 [1, 2] 에 추가
let numbers: [Int] = [1, 2, 3, 4]
// map과 filter 이용
var doulberNums: [Int] = [1, 2] + numbers.filter(){$0 % 2 == 0}.map(){$0 * 2}
print(doulberNums)
// reduce 이용
doulberNums = numbers.reduce(into: [1, 2]){
// 짝수만 걸러내기
guard $1.isMultiple(of: 2) else {
// 2의 배수가 아니면
return
}
// 초기값에 두배해서 추가
$0.append($1 * 2)
}
print(doulberNums)
// 문자열을 모두 대문자로 변경
let names: [String] = ["Kim", "Lee", "Park"]
// map 이용
var upperNames: [String] = names.map(){ $0.uppercased() }
print(upperNames)
// reduce 이용
upperNames = names.reduce(into: []){ $0.append($1.uppercased())}
print(upperNames)
<실행결과>
[1, 2, 4, 8]
[1, 2, 4, 8]
["KIM", "LEE", "PARK"]
["KIM", "LEE", "PARK"]
* 예시 - map, filter, reduce 모두 사용
// 짝수를 걸러내서 3을 곱한 후 모두 더하기
let numbers: [Int] = [1, 2, 3, 4, 5, 6]
let result: Int = numbers.filter(){$0 % 2 == 0}.map(){$0 * 3}.reduce(0){$0 + $1}
print(result)
<실행결과>
36
* 예시 - map, filter, reduce 모두 활용
// 성별
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: "Kim", gender: .male, location: "전남", age: 26))
friends.append(Friend(name: "Lee", gender: .male, location: "강원", age: 24))
friends.append(Friend(name: "Park", gender: .male, location: "경기", age: 30))
friends.append(Friend(name: "Choi", gender: .female, location: "서울", age: 22))
friends.append(Friend(name: "Kang", gender: .male, location: "충북", age: 20))
friends.append(Friend(name: "Son", gender: .unknown, location: "대전", age: 29))
friends.append(Friend(name: "Hwang", gender: .male, location: "경기", age: 24))
-> 친구에 관한 정보가 다음과 같을 때 다음 년도 기준으로 서울 외에 거주하는 25세 이상의 친구 출력
// 서울 외의 지역에 거주하는 25세 이상의 친구
var myFriends: String = friends.map(){
Friend(name: $0.name, gender: $0.gender, location: $0.location,
age: $0.age + 1)
}.filter(){
$0.location != "서울" && $0.age >= 25
}.reduce("my Friends\n"){
$0 + "\($1.name) \($1.gender) \($1.location) \($1.age)세\n"
}
print(myFriends)
<실행결과>
my Friends
Kim male 전남 27세
Lee male 강원 25세
Park male 경기 31세
Son unknown 대전 30세
Hwang male 경기 25세
댓글
댓글 쓰기