스위프트나 코틀린 같은 함수형 패러다임 언어는 컬렉션을 가공하는 수많은 유용한 함수를 제공해준다. 예를 들어 배열에서 특정 조건을 만족하는 요소만 담아서 새로운 배열을 만들어준다던가, 배열의 모든 요소에 특정 작업을 수행할 수 있다. 스위프트에서 유용한 고차함수로 Map, Filter, Reduce 가 있는데, 이것을 활용해 데이터 연산을 쉽게 해보자!
Map
맵은 자신을 호출할 때 매개변수로 전달된 함수를 실행하여 그 결과를 다시 반환해주는 함수이다
Swift 에서 map 함수는 Sequence, Collection 프로토콜을 따르는 타입과 옵셔널은 모두 맵을 사용할 수 있다. map 을 사용하면 컨테이너가 담고 있던 각각의 값을 매개변수로 전달한 함수를 실행시켜 새로운 값으로 변환하고 다시 컨테이너에 담아서 반환한다. 여기서 주의할 것이 있는데, 기존 컨테이너의 값은 전혀 변하지 않고 새로운 컨테이너를 생성해 반환한다는 점이다.
@inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
map 함수의 정의다. 매개변수로 컨테이너의 각 요소에 함수를 적용해 T 타입, 즉 새로운 타입을 반환하는 함수를 받는다. 즉 배열에 map 을 적용한다고 하면 배열의 모든 요소에 transform 함수를 적용해서 함수가 반환하는 새로운 타입의 배열로 반환한다는 뜻이다. 간단한 코드로 사용법을 알아보자
map 함수의 기본 사용법
let list = [10,20,30,40,50,60,70,80,90,100]
let doubledList = list.map { value in
return value * 2
}
print(doubledList.description)
//출력 결과
[20, 40, 60, 80, 100, 120, 140, 160, 180, 200]
정수를 담고 있는 배열 list 를 선언하고, list 에 map 함수를 호출했다. list의 각 원소인 value 를 두 배 곱해서 반환하는 함수를 전달하면, 새로운 doubledList 엔 모든 원소가 2배가 된 리스트가 반환된다. 물론 for - in 구문을 사용해서도 위 코드와 동일한 결과를 얻을 수 있다.
let list = [10,20,30,40,50,60,70,80,90,100]
var doubledList : [Int] = [Int]()
for number in list {
doubledList.append(number * 2)
}
print(doubledList.description)
//출력 결과는 위 코드와 동일하다!
하지만 map 함수를 사용하면 다중 스레드 환경에서 동기화 문제를 신경쓰지 않아도 되고, 코드의 재사용 측면, 컴파일러 최적화 측면에서 많은 이점을 가져갈 수 있다. for - in 구문으로 구현한 코드를 보면, 처음부터 빈 배열을 생성해야 하고, 새로운 배열에 append 연산을 계속해서 실행해줘야 한다.
map 함수 좀 더 쉽게 사용하기
map 함수가 for - in 구문보다 가독성이 좋고 코드가 간결한건 변하지 않는 사실이다. 우리는 후행 클로저, 암시적 반환, 매개변수 생략 등의 개념을 활용해서 위 코드를 더욱 간결하게 만들 수 있다.
let list = [10,20,30,40,50,60,70,80,90,100]
//후행 클로저, 암시적 반환, 매개변수 생략
var doubledList : [Int] = list.map{$0 * 2}
print(doubledList.description)
클로저의 반복 사용
위에서 map 함수를 사용하면 코드의 재사용 측면에서 더 많은 이점을 가져갈 수 있다고 했다. 같은 기능을 사용할 것이라면 매 번 새로운 클로저를 생성하는 것보다, 하나의 클로저를 미리 생성해서 여러 컬렉션에 전달해주는 것이 좋을 것이다. 각 원소를 2배로 만들어 반환하는 함수를 미리 만들어보자.
//2를 곱하는 함수를 미리 생성한다!
let multiplyTwo : (Int) -> Int = {$0 * 2}
let list = [10,20,30,40,50,60,70,80,90,100]
let oddList = [1,3,5,7,9]
let doubledList : [Int] = list.map(multiplyTwo) //[20,40,60,80,100...]
let doubledOddList = oddList.map(multiplyTwo) //[2,6,10,14,18]
물론 다음과 같은 방식으로 곱할 숫자를 지정해주는 함수를 선언해서 사용할 수도 있다.
func makeMultiplier(_ mul:Int) -> (Int) -> Int {
{$0 * mul}
}
let list = [10,20,30,40,50,60,70,80,90,100]
let oddList = [1,3,5,7,9]
let doubledList : [Int] = list.map(makeMultiplier(5)) //50,100,150...
let doubledOddList = oddList.map(makeMultiplier(10)) //10,30,50,70,90
다양한 컬렉션에 사용해보기
Dictionary 에 사용해보자
let dictionary = ["apple":"황플",
"samsung":"삼성",
"google":"구글"]
let meanArray = dictionary.map { $0.value }
print(meanArray.description)
//출력 결과
["구글","황플","삼성"]
위 코드는 dictionary 에서 value 만 추출하는 코드로, 정상적으로 value만 빼낸 배열이 생성됨을 알 수 있다. 그러나 역시 Dictionary 라서 순서는 보장되지 않는다. 실행 때마다 value 순서가 계속해서 달라진다.
filter
filter 는 특정 조건을 만족하는 요소만 컨테이너에 담아 반환한다
filter 는 말 그대로 컨테이너 내부의 값을 걸러서 추출하는 역할을 한다. map 과 마찬가지로 새로운 컨테이너에 값을 담아 반환하지만, 차이점이 있다면 특정 조건을 만족하는 요소만 컨테이너에 담는다는 점이다. filter 함수의 정의를 보도록 하자.
@inlinable public func filter(_ isIncluded: (Element) throws -> Bool) rethrows -> [Element]
map 과 다르게 매개변수로 받는 함수의 반환타입이 Bool 인 것을 알 수 있다. 즉 해당 요소를 담을 지를 Bool 타입을 반환해서 알려주는 것이다.
filter 의 사용법
let arr = [1,2,3,4,5]
let largerThanThree = arr.filter { value in
return value > 3
}
print(largerThanThree.description)
//출력 결과
[4,5]
3보다 큰 원소만 담도록 filter 에 함수를 전달해주었다. 만약 사전에 원소를 변형한 뒤에 filter 를 호출하고 싶다면 map 함수를 먼저 실행한 뒤 filter 함수를 실행한다.
let arr = [1,2,3,4,5]
let doubledList = arr.map{$0*2}
let largerThanThree = doubledList.filter { value in
return value > 3
}
print(largerThanThree.description)
//출력 결과
[4,6,8,10]
그냥 메소드 체이닝으로 새로운 배열을 선언할 필요 없이 동일한 결과를 얻을 수도 있다.
let arr = [1,2,3,4,5]
//map과 filter 를 연결해서 사용하자!
let largerThanThree = arr.map{$0*2}.filter{$0 > 3}
print(largerThanThree.description)
//출력 결과
이전과 동일하게 [4,6,8,10]
Reduce
Reduce 는 컨테이너 내부의 요소를 하나로 합치는 고차함수다
Reduce 는 사실 결합이라고 불러야 마땅한 기능이다. 왜냐면 컨테이너 내부의 요소를 하나로 합치는 기능을 실행하기 때문이다. 만약 배열이라면 배열의 모든 값을 전달인자로 전달받은 클로저의 연산 결과로 합해준다. reduce의 정의를 살펴보자
public func reduce<Result>(_ initialResult : Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result
초기값을 받아서, nextPartialResult 클로저로 계속해서 이전 클로저의 결과값에 함수를 적용한다. nextPartialResult 함수는 (Result, Element) -> Result 타입으로 여기서 Result 매개변수는 이전 클로저의 실행 결과값이고, Element 는 현재 컨테이너의 원소다. 컨테이너의 모든 원소에 대한 순회가 끝나면, 단 하나의 Result 를 반환한다. Reduce 는 위 정의뿐 아니라 한 가지의 정의가 더 있다.
public func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) throws -> ()) rethrows -> Result
위 함수는 이전 정의와 마찬가지로 초기값을 전달받고, updateAccumulatingResult 함수를 통해 계속해서 결과를 갱신한다. 다른점이 있다면 updateAccumulatingResult 함수의 매개변수가 inout 타입이라는 것과 반환값이 없다는 것이다. 즉 Result 의 주소를 계속 참조해서 해당 값에 계속해서 연산을 진행한다.
reduce 의 사용법
let arr = [1,2,3,4,5]
let sumOfArr = arr.reduce(0){ result, value in
result + value
}
print(sumOfArr) //15
초기값으로 0을 주고, 배열의 모든 원소를 더한 값을 상수에 저장했다. 문자열 배열을 단 하나의 문자열로 변환할 수도 있다.
let stringArr = ["Apple","is","the","best","company","in","the","world"]
let bible = stringArr.reduce("Truth:"){
return "\($0) \($1)"
}
print(bible) // Truth: Apple is the best company in the world
reduce 또한 map, filter 와 결합해서 사용할 수 있다. 각 배열의 원소를 3배 곱한 다음, 5보다 큰 원소만 모두 더한 값을 구한다고 생각해보자.
let arr = [1,2,3,4,5]
let sum = arr.map{$0 * 3}.filter{$0 > 5}.reduce(0){$0 + $1}
print(sum) //42
map 연산에 의해 배열은 [3,6,9,12,15] 가 되고 5보다 큰 원소인 6 + 9 + 12 + 15 = 42인 결과가 반환되는 것을 확인할 수 있다.
map, filter, reduce 의 활용
enum Gender {
case male,female,androdjean,newtroys,ay_gender,genderless,bygender,trigender,gender_fluid
}
struct Friend {
let name:String
let gender: Gender
let location : String
var age : UInt
}
var friends: [Friend] = [Friend]()
friends.append(Friend(name: "HoJong", gender: .male, location: "대구", age: 27))
friends.append(Friend(name: "UkDong", gender: .androdjean, location: "대구", age: 48))
friends.append(Friend(name: "YoungHoon", gender: .trigender, location: "서울", age: 36))
friends.append(Friend(name: "SiJin", gender: .male, location: "서울", age: 23))
friends.append(Friend(name: "HoHo", gender: .trigender, location: "서울", age: 19))
friends.append(Friend(name: "Hohisqd", gender: .trigender, location: "인천", age: 23))
friends.append(Friend(name: "Abdul", gender: .trigender, location: "대구", age: 4))
friends.append(Friend(name: "Claim", gender: .trigender, location: "서울", age: 125))
friends.append(Friend(name: "BIRDS", gender: .trigender, location: "서울", age: 53))
friends.append(Friend(name: "DenzelCurry", gender: .male, location: "서울", age: 25))
위 데이터는 작년 자료다. 따라서 올해는 친구들의 나이가 1씩 증가했을 것이다. 이 때 서울 외의 지역에 거주하며 나이가 20세 이상인 친구를 찾아보자.
let result = friends.map{
Friend(name: $0.name, gender: $0.gender, location: $0.location, age: $0.age+1)
}.filter {
$0.location != "서울" && $0.age >= 20
}.reduce("지방에 거주하며 나이가 20세 이상인 친구들: "){
"\($0)\n\($1.name) \($1.age) \($1.location) \($1.gender)"
}
print(result)
//출력 결과
지방에 거주하며 나이가 20세 이상인 친구들:
HoJong 28 대구 male
UkDong 49 대구 androdjean
Hohisqd 24 인천 trigender
map, filter, reduce 를 적재적소에 활용하여 고급 개발자가 되도록 하자!
'Swift' 카테고리의 다른 글
Swift - 프로토콜 1 (0) | 2022.02.19 |
---|---|
Swift - 서브스크립트 (0) | 2022.02.18 |
Swift - 옵셔널 체이닝, guard (0) | 2022.02.16 |
Swift - 클로저 (0) | 2022.02.15 |
Swift 옵셔널 (0) | 2022.02.14 |
Comment