오늘은 고차함수에 대해서 알아보자.
Swift에서 제공하는 고차함수인 Map, Filter, Reduce를 자주 사용했었는데 이참에 정리해두려 한다.
고차함수는 다른 함수를 전달인자로 받거나 함수 실행의 결과를 함수로 반환하는 함수이다.
Swift에서 제공하는 고차함수로는 Map, Filter, Reduce가 있다.
하나씩 알아가보면서 고차함수를 이해해보자.
map은 컨테이너 내부의 기존 데이터를 변형하여 새로운 컨테이너를 생성한다.
이렇게 말하면 무슨 말인지 잘 모르겠다.
Int형 배열을 String형 배열로 바꾸는 코드를 통해 이해해보자!
let numbers: [Int] = [0, 1, 2, 3, 4]
var stringNumbers: [String] = []
for number in numbers {
stringNumbers.append(number)
}
print(stringNumbers)
// 결과
["0", "1", "2", "3", "4"]
let numbers: [Int] = [0, 1, 2, 3, 4]
let stringNumbers = numbers.map({(number: Int) -> String in
return "\(number)"
})
print(stringNumbers)
// 결과
["0", "1", "2", "3", "4"]
태초에 Int 형 배열 numbers가 있었다..ㅋㅋㅋ
우리가 map으로 변형한 stringNumbers는 어떤 타입의 배열일까?
결과를 보면 String 타입임을 알 수 있다.
그렇다면 map을 통해 [Int]가 [String]으로 변형됐다는 뜻인데 한 번 봐보자.
numbers.map({(number: Int) -> String in
return "\(number)"
})
위의 코드를 봐보자.
()안에 클로저를 넘겨주고 있다.
클로저를 확인해보니 Int를 String으로 바꿔주는거 같다.
그렇다면 위의 map과 같은 기능을 하도록 for문을 사용해 코드를 짤 수 있을까?
그렇다면 numbers: [Int]에서 요소 하나하나를 돌면서 [String]으로 변형해주고 있구나를 알 수 있다.
그렇다면 numbers에 Optional이 있다면 어떻게 될까?
let numbers: [Int?] = [0, 1, 2, nil, 4]
let stringNumbers = numbers.map({(number: Int?) -> String in
return "\(number)"
})
print(stringNumbers)
// 결과
["Optional(0)", "Optional(1)", "Optional(2)", "nil", "Optional(4)"]
nil을 넣기 위해 number의 타입을 [Int?]로 변경했다.
nil이 아닌 값은 Optional String으로 변환되지만 nil인 값은 똑같이 nil임을 확인할 수 있다.
그렇다면 nil을 없애려면 어떻게 할 수 있을까?
let numbers: [Int?] = [0, 1, 2, nil, 4]
var stringNumbers: [String] = []
for number in numbers {
if let n = number {
stringNumbers.append("\(n)")
}
}
for문과 if let을 이용할 수 있지만 더 Cool한 방법이 있다!
let numbers: [Int?] = [0, 1, 2, nil, 4]
let stringNumbers = numbers.compactMap { number in
return number
}
print(stringNumbers)
// 결과
[0, 1, 2, 4]
compactMap을 사용하면 nil이 제거된 결과를 확인할 수 있다.
그렇다면 2차원 배열일때는 어떨까?
let numbers: [[Int?]] = [[0, 1, 2, nil, 4], [5, 6, 7, nil, 8]]
let stringNumbers = numbers.compactMap { number in
return number
}
print(stringNumbers)
// 결과
[[Optional(0), Optional(1), Optional(2), nil, Optional(4)], [Optional(5), Optional(6), Optional(7), nil, Optional(8)]]
2차원 배열을 유지하고 있다.
nil이 제거되지 않는 것을 확인할 수 있다.
let numbers: [Int?] = [0, 1, 2, nil, 4]
let stringNumbers = numbers.flatMap { number in
return number
}
print(stringNumbers)
// 결과
[0, 1, 2, 4]
flatMap을 사용해도 nil이 제거된 결과를 확인할 수 있는데 그렇다면 2차원 배열일때는 어떨까?
let numbers: [[Int?]] = [[0, 1, 2, nil, 4], [5, 6, 7, nil, 8]]
let stringNumbers = numbers.flatMap { number in
return number
}
print(stringNumbers)
// 결과
[Optional(0), Optional(1), Optional(2), nil, Optional(4), Optional(5), Optional(6), Optional(7), nil, Optional(8)]
오잉...?!
2차원 배열이 1차원 배열로 변했다...!
flat은 평면 이라는 뜻으로 flatMap도 2차원 배열을 1차원 배열로 평평하게 만들어 주고 있다.
1차원 배열에서는 compactMap과 flatMap이 동일한 결과를 보여주고 있는데 왜 그럴까?
기존의 flatMap은 배열을
하는 역할을 했다.
이중에서 "nil을 제거"하는 기능을 compactMap이 하게 되었다.
let numbers: [[Int?]] = [[0, 1, 2, nil, 4], [5, 6, 7, nil, 8]]
var forStringNumbers: [[Int]] = []
for number in numbers {
forStringNumbers.append(number.compactMap{ n in
return n
})
}
print(forStringNumbers)
// 결과
[[0, 1, 2, 4], [5, 6, 7, 8]]
for문과 compactMap을 조합해 2차원 배열을 유지하고 nil을 제거할 수 있다.
let numbers: [[Int?]] = [[0, 1, 2, nil, 4], [5, 6, 7, nil, 8]]
let compactFlatStringNumbers = numbers.flatMap{ $0 }.compactMap{ $0 }
print(compactFlatStringNumbers)
// 결과
[0, 1, 2, 4, 5, 6, 7, 8]
flatMap과 compactMap를 조합해 nil을 제거한 1차원 배열을 얻을 수 있다.
filter는 컨테이너 내부의 값을 걸러서 새롤운 컨테이너로 추출한다.
새로운 컨테이너에 담아서 반환한다.
map은 기존의 data를 변형해서 새로운 컨테이너에 담아서 반환하지만
filter는 기존 data를 그대로 가져와서 담아서 반환한다.
filter 함수의 매개변수로 전달되는 함수의 반환 타입은 Bool이다.
filter 함수를 확인하기 전에for문으로 먼저 구현해보자.
numbers 배열에서 짝수만 반환하는 배열을 구해보자.
let numbers = [0, 1, 2, 3, 4]
var filterNumbers: [Int] = []
for number in numbers {
if number % 2 == 0 {
filterNumbers.append(number)
}
}
// 결과
[0, 2, 4]
let numbers = [0, 1, 2, 3, 4]
let filterNumbers = numbers.filter { (number: Int) -> Bool in
return number % 2 == 0
}
print(filterNumbers)
// 결과
[0, 2, 4]
number % 2 == 0이라면 filter된 배열에 담기게 된다.
즉, 짝수만 배열에 담겨 반환된다.
reduce는 컨테이너 내부의 콘텐츠를 하나로 통합한다.
reduce를 사용할때는 초기값을 줘야한다.
numbers 배열의 요소를 다 더한 값을 구해보자.
let numbers = [0, 1, 2, 3, 4]
var reduceNumber = 0
for number in numbers {
reduceNumber = reduceNumber + number
}
print(reduceNumber)
// 결과
10
let numbers = [0, 1, 2, 3, 4]
let reduceNumber = numbers.reduce(0, { (first: Int, second: Int) -> Int in
return first + second
})
print(reduceNumber)
// 결과
10
0이라는 초기값을 주었다.
그렇다면 왜 위와 같은 고차함수를 사용할까?
기존에 for문을 통해서도 구현할 수 있는데!
위에서 for문을 이용해 구현한 코드들의 특징이 있다.
반환할 프로퍼티들은 모두 var(변수)로 정의했다.
for문을 돌면서 적합한 요소들을 append(reduce 말고..)해주기 위해 변수로 선언했다.
고차함수를 이용해 구현한 코드들은 let(상수)로 정의되어 있다.
for문을 사용한다면 상수로 표현할 수 없지만 고차함수를 사용한다면 상수로 표현할 수 있다.
이렇게 상수로 사용한다면 Compile시 유리하다.
오늘은 고차함수에 대해서 알아봤다.
프로젝트를 진행하면서 고차함수를 자주 쓰게 되는데 굉장히 편하고, 함수의 뎁스를 줄일 수 있는 방법 중 하나라고 생각한다.
오늘 정리했으니 앞으로는 헷갈리지 않고 잘 쓸 수 있도록...!
그럼 이만👋