오늘은 고차함수에 대해서 알아보자.
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시 유리하다.
오늘은 고차함수에 대해서 알아봤다.
프로젝트를 진행하면서 고차함수를 자주 쓰게 되는데 굉장히 편하고, 함수의 뎁스를 줄일 수 있는 방법 중 하나라고 생각한다.
오늘 정리했으니 앞으로는 헷갈리지 않고 잘 쓸 수 있도록...!
그럼 이만👋