1급 객체(First class citizen)와 고차함수(고계함수)
프로그래밍 언어에서 1급 객체란 아래의 조건을 충족시키면 됩니다. 1. 변수나 데이터에 할당 할 수 있어야 한다. 2. 객체의 매개변수로 넘길 수 있어야 한다. 3. 객체의 반환값으로 리턴 할수 있
swifty-cody.tistory.com
이전에 썼던 고차함수 글에 이어서,
Swift 표준 라이브러리에서 지원하는 고차함수인 filter, reduce, map을 정리해보겠습니다.
이 고차함수들은 컨테이너 타입(Array, Dictionary, Set, ...)에 구현되어 있는 제네릭 함수입니다.
우리가 주로 for문을 돌면서 어떤 결과를 추려낼 때 하던 작업을 이 함수들로 대체할 수 있습니다.
// 예시로 사용할 컨테이너
let someNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
filter
filter는 컨테이너의 값을 걸러서 새로운 컨테이너로 내보내줍니다.
이름에서도 느껴지듯이 필터링을 해줍니다.
배열 내에 있는 'Element타입을 받아서 Bool을 반환하는 클로저'를 넣어주면 Element배열을 내보내주는데,
이 클로저에 Element값을 내보내지는 배열에 포함할지를 결정할 수 있는 조건문을 반환하도록 작성해주면 됩니다.
먼저 기존의 for문을 사용해서 위 예시 컨테이너의 값을 필터링하기 위해 아래와 같이 작성해봅니다.
// for문 사용
var filtered: [Int] = [Int]()
for number in someNumbers {
if number % 2 == 0 { // 짝수만 필터링
filtered.append(number)
}
}
print(filtered) // [0, 2, 4, 6, 8]
아래는 filter를 사용해서 작성한 것입니다.
// filter 사용
// numbers의 요소 중 짝수를 걸러내어 새로운 배열로 반환
let evenNumbers: [Int] = someNumbers.filter ({ (number: Int) -> Bool in
return number % 2 == 0
})
print(evenNumbers) // [0, 2, 4, 6, 8]
filter에는 어떤 클로저를 넣어야하는지 타입추론이 가능하기 때문에 매개변수와 반환 타입을 생략이 가능하고,
해당 매개변수를 축약인자 $0로 받아서 사용할 수 있습니다.
클로저 외에 넣는 매개변수가 없기 때문에 후행 클로저 규칙으로 소괄호()를 생략이 가능합니다.
아래는 단일 표현으로 return 키워드까지 생략하여 작성되었습니다.
// 매개변수, 반환 타입, 반환 키워드(return) 생략, 후행 클로저
let oddNumbers: [Int] = someNumbers.filter {
$0 % 2 != 0
}
print(oddNumbers) // [1, 3, 5, 7 ,9]
reduce
reduce는 컨테이너의 값들을 하나의 값으로 줄이는데 사용됩니다.
reduce는 두개의 매개변수를 받습니다. 첫번째 매개변수는 초기값(Result)입니다.
두번째 매개변수는 기존 결과값(Result)과 현재값(Element)를 받아서 새 Result를 반환하는 클로저입니다.
숫자 컨테이너의 경우 첫 매개변수(초기값 Result)에 보통 0을 넣고, 해당 컨테이너 외의 값에 결과를 이어서 계산하고 싶다면 해당 값을 넣어주면 됩니다.
그러면 두번째 매개변수 클로저로 해당 초기값을 컨테이너의 값과 함께 넘겨받게 됩니다.
먼저 기존의 for문을 사용해서 위 예시 컨테이너의 값을 모두 더한 값으로 줄이기 위해 아래와 같이 작성해봅니다.
// for문 사용
var result: Int = 0
// someNumbers의 모든 요소를 더함
for number in someNumbers {
result += number
}
print(result) // 45
아래는 reduce를 사용해서 작성한 것입니다.
// reduce 사용
// 초기값이 0이고 someNumbers 내부의 모든 값을 더함
let sum: Int = someNumbers.reduce(0, { (result: Int, currentItem: Int) -> Int in
return result + currentItem
})
print(sum) // 45
위 reduce는 아래처럼 동작하여 결과값을 내놓습니다.
첫 currentItem은 reduce첫 인수로 받은 0을 받아서 계산하고,
두번째 currentItem부터는 앞단계에서의 결과값(return)을 받아서 계산하게 됩니다.
result | currentItem | return |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 2 | 3 |
3 | 3 | 6 |
6 | 4 | 10 |
... | ... | ... |
36 | 9 | 45 |
reduce역시 어떤 매개변수를 넣어야하는지 타입추론이 가능하여 매개변수와 반환 타입을 생략이 가능하고,
해당 매개변수를 순서대로 축약인자 $0, $1로 받아서 사용할 수 있습니다.
클로저가 후행 클로저 규칙으로 소괄호()를 생략이 가능합니다.
아래는 단일 표현으로 return 키워드까지 생략하여 작성되었습니다.
// 초깃값이 0이고 someNumbers 내부의 모든 값을 뺌
let subtract = someNumbers.reduce(0) { $0 - $1 }
print(subtract) // -45
map
map은 컨테이너의 기존 데이터를 변형(transform)하여 새로운 컨테이너를 생성해 반환합니다.
배열 내에 있는 'Element타입을 받아서 T타입을 반환하는 클로저'를 넣어주면 T타입의 배열을 내보내주는데,
이 클로저에 Element값을 가공하여 새로운 값 T를 반환하도록 작성해주면 됩니다.
먼저 기존의 for문을 사용해서 위 예시 컨테이너의 값을 변형하기 위해 아래와 같이 작성해봅니다.
기존의 값을 x2한 값을 반환하는 map입니다.
// for문을 사용
var multiple = [Int]()
for number in someNumbers {
multiple.append(number * 2)
}
print(multiple) // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
아래는 map을 사용해서 작성한 것입니다.
// map을 사용
// someNumbers 각 요소를 2배하여 새로운 배열 반환
multiple = someNumbers.map({ (number: Int) -> Int in
return number * 2
})
print(multiple) // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
map도 어떤 클로저를 넣어야하는지 타입추론이 가능하기 때문에 매개변수와 반환 타입을 생략이 가능하고,
해당 매개변수를 축약인자 $0로 받아서 사용할 수 있습니다.
클로저 외에 넣는 매개변수가 없기 때문에 후행 클로저 규칙으로 소괄호()를 생략이 가능합니다.
아래는 단일 표현으로 return 키워드까지 생략하여 작성되었습니다.
// someNumbers의 각 요소를 3배하여 새로운 배열 반환
let triple = someNumbers.map { $0 * 3 }
print(triple) // [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]
그리고 flatMap & compactMap
map에서 파생된 형태로 flatMap과 compactMap이 있습니다.
위에서 설명한 map과 기본적인 동작은 같지만, 추가적인 기능이 있습니다.
flatMap
flatMap은 2차원 배열일 때, 1차원 배열로 flat하게 만들어줍니다.
이 때 배열 내에 nil이 있을 때 제거해주지는 않습니다.
let overlayedArray: [[Int?]] = [[1, 2, 3], [nil, 5], [6, nil], [nil, nil]]
let flatMapTest = overlayedArray.flatMap { $0 }
print(flatMapTest)
// [Optional(1), Optional(2), Optional(3), nil, Optional(5), Optional(6), nil, nil, nil]
1차원 배열에 flatMap을 쓰면 어떻게 될까요?
swift4.1부터 deprecated되었다면서, compactMap(_:)을 사용하라는 경고가 뜹니다. 하지만 일단 결과값은 나오는데, nil이 제거가 되어 있습니다. 4.1 이전에는 해당 기능이 flatMap에 있었지만 그 이후부터는 compactMap으로 이전되었습니다.
let array = [1, nil, 3, nil, 5, 6, 7]
let flatMapTest = array.flatMap{ $0 } // 'flatMap' is deprecated: Please use compactMap(_:) for the case where closure returns an optional value
print(flatMapTest) // [1, 3, 5, 6, 7]
compactMap
compactMap은 (위에도 썼다시피 swift 4.1이전에 flatMap에 있던)
'1차원 배열의 nil값을 제거한 배열을 반환'해주는 기능을 가지고 있습니다.
let array = [1, nil, 3, nil, 5, 6, 7]
let compactMapTest = array.compactMap{ $0 }
print(compactMapTest) // [1, 3, 5, 6, 7]
이번엔 compactMap을 2차원 배열에서 쓰면 어떻게 될까요? 옵셔널 바인딩만 걸립니다.
let overlayedArray: [[Int?]] = [[1, 2, 3], [nil, 5], [6, nil], [nil, nil]]
let compactMapTest = overlayedArray.compactMap { $0 }
print(compactMapTest)
// [[Optional(1), Optional(2), Optional(3)], [nil, Optional(5)], [Optional(6), nil], [nil, nil]]
고차함수들의 chaining
그렇다면 2차원 배열인데 nil도 제거하고 싶고, 1차원 배열로 flat하게 만들어주고 싶다면?
앞에서 정리한 flatMap과 compactMap을 체이닝으로 사용하면 됩니다.
let array: [[Int?]] = [[1, 2, 3], [nil, 5], [6, nil], [nil, nil]]
let flatCompactMapTest = array.flatMap { $0 }.compactMap({ $0 })
print(flatCompactMapTest) // [1, 2, 3, 5, 6]
filter, map, reduce과 같은 함수들도 마찬가지입니다.
컨테이너의 값을 홀수만 filtering해서, x2를 한 값들을 모두 더하고 싶다면, 아래처럼 체이닝을 하면 됩니다.
let someNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let result = someNumbers.filter { $0 % 2 != 0 } // [1, 3, 5, 7, 9]
.map { $0 * 2 } // [2, 6, 10, 14, 18]
.reduce(0) { $0 + $1 } // 50
print(result) // 50
[ 본 글을 읽는데 도움이 되는 다른 글]
제네릭 Generic
제네릭 Generic은 단어의 뜻이 '포괄적인, 통칭의'라는 의미입니다. 단어의 의미처럼 제네릭은 타입에 종속적이지 않도록 Swift 코드를 좀 더 유연하고, 재사용 가능한 함수 및 코드를 작성할 수 있
swifty-cody.tistory.com
클로저 Closure
클로저(Closure)는 코드 블럭 { } 으로 Objective-C의 block이나 타 언어의 람다, 익명함수와 유사한 개념입니다. Swift에서는 함수가 '이름이 있는 클로저'라고 하는 편이 맞습니다. 이전 글에서 Swift에서
swifty-cody.tistory.com
1급 객체(First class citizen)와 고차함수(고계함수)
프로그래밍 언어에서 1급 객체란 아래의 조건을 충족시키면 됩니다. 1. 변수나 데이터에 할당 할 수 있어야 한다. 2. 객체의 매개변수로 넘길 수 있어야 한다. 3. 객체의 반환값으로 리턴 할수 있
swifty-cody.tistory.com