상세 컨텐츠

본문 제목

Swift 15장 맵, 필터, 리듀스

iOS/Swift

by 쑤야. 2022. 11. 13. 14:48

본문

매개변수로 함수를 갖는 함수를 고차 함수라고 부름

 

Swift에서 유용한 고차함수로는 맵, 필터, 리듀스 등이 존재

 

1. 맵(map)

 👉🏻 자신을 호출할 때 매개변수로 전달된 함수를 실행하여 그 결과를 다시 반환해주는 함수


Swift의 Sequence, Collection 프로토콜을 따르는 타입과 옵셔널은 모두 맵을 사용할 수 있음

맵을 사용하면 컨테이너가 담고 있던 각각의 값을 매개변수를 통해 받은 함수에 적용한 후, 다시 컨테이너에 포장하여 반환한다. 
즉, 기존 컨테이너의 값은 변경되지 않고 새로운 컨테이너가 생성되어 반환되는 것이다. 

따라서 맵은 기존 데이터를 변형하는 데 많이 사용한다

map 메서드의 사용법은 for-in 구문과 별 차이가 없지만, 

 

  1. 코드의 재사용 측면이나 컴파일러 최적화 측면에서 차이가 발생
  2. 다중 스레드 환경일 때 대상 컨테이너의 값이 스레드에서 변경되는 시점에 다른 스레드에서도 동시에 값이 변경되려고 할 때, 예측하지 못한 결과가 발생하는 부작용을 방지할 수도 있음
  3. for-in 구문을 사용하기 위해 빈 배열을 선언하는 작업이 필요없으며, 배열 append 연산을 실행하기 위한 시간도 필요없음

 

아래 예제를 살펴보겠다. 

let numbers: [Int] = [0,1,2,3,4]

 

1. for-in 구문 사용

var doubledNumbers: [Int] = [Int]() //for-in 구문 결과값 저장 위한 빈 배열 선언

for number in numbers{
    doubledNumbers.append(number * 2)
}

2. map 메서드 사용

var doubledNumbers = numbers.map({ (number: Int) -> Int in
    return number * 2
})

 

 * 클로저 표현의 간략화

//기본 형태
doubledNumbers = numbers.map({ (number: Int) -> Int in
    return number * 2
})

//매개변수 및 반환 타입 생략
doubledNumbers = numbers.map({ return $0 * 2 })

//반환 키워드 생략
doubledNumbers = numbers.map({ $0 * 2 })

//후행 클로저 사용
doubledNumbers = numbers.map { $0 * 2 }

 

재사용 측면을 고려한다면,

같은 기능을 여러 번 사용하기 위해 하나의 클로저 선언하고, 여러 map 메서드에서 사용하는 것이 좋을 수 있음

let multiplyTwo: (Int) -> Int = { $0 * 2 }

doubledNumbers = numbers.map(multiplyTwo)

 

2. 필터(filter)

👉🏻 컨테이너 내부의 값을 걸러서 추출하는 역할을 하는 고차함수

 

맵과 마찬가지로 새로운 컨테이너에 값을 담아 반환해준다

 

다만 맵처럼 기존 콘텐츠를 변형하는 것이 아니라, 특정 조건에 맞게 걸러내는 역할을 수행

 

filter 함수의 매개변수로 전달되는 함수의 반환 타입은 Bool로, 새로운 컨테이너에 포함될 항목이라고 판단하면 true를, 아닐 경우 false를 반환한다

let numbers: [Int] = [0,1,2,3,4]
let evenNumbers: [Int] = numbers.filter { (number: Int) -> Bool in
    return number % 2 == 0
}

 

콘텐츠의 변형 후에 필터링을 진행하고 싶은 경우, 맵을 사용한 후에 필터 메서드를 호출하면 됨

let mappedNumbers: [Int] = numbers.map{ $0 + 3 }

let evenNubmers: [Int] = mappedNumbers.filter { (number: Int) -> Bool in
    return number % 2 == 0
}

 

위의 코드에서 mappedNumbers를 여러 번 사용하는게 아니라면, 메서드를 체인처럼 연결하여 사용할 수도 있음

let oddNumbers: [Int] = numbers**.map**{ $0 + 3 }**.filter**{ $0 % 2 == 1}

3. 리듀스(reduce)

👉🏻 컨테이너 내부의 콘텐츠를 하나로 합하는 기능을 실행하는 고차함수

 

결합이라고 불릴 수 있는 기능으로,

배열이라면 배열의 모든 값을 전달인자로 전달받은 클로저의 연산 결과로 합해줌

 

Swift의 리듀스(reduce)는 2가지 형태로 구현되어 있음

 

1. 클로저가 각 요소를 전달받아 연산한 후 값을 다음 클로저 실행을 위해 반환하며 컨테이너를 순환하는 형태

func reduce<Result>( _ initialResult: Result, _ nextPartialResult: (Result, Self.Element) throws -> Result ) rethrows -> Result
  • initialResult
    : 초기값 지정
  • nextPartialResult
    : Result로, 클로저 결과를 전달 받음
    • Result모든 순회가 끝나면 리듀스의 최종 결괏값이 됨
      : 리듀스 메서드의 initialResult 매개변수를 통해 전달받은 초깃값 또는 이전 클로즈의 결괏값
    • Element
      : 리듀스 메서드가 순환하는 컨테이너의 요소
let numbers = [1, 2, 3, 4]

let numberSum = numbers.reduce(0, { x, y in
    x + y
})
// numberSum == 10
let numbers: [Int] = [1,2,3]

var subtractFromThree: Int = numbers.reduce(3){ //클로저 간략화 표현
    return $0 - $1
}
// subtractFromThree == -3

 

2. 컨테이너를 순환하며 클로저가 실행되지만 클로저가 따로 결괏값을 반환하지 않는 형태

    → 클로저 내부에서 이전 값을 변경하는 식으로 코드를 작성하게 된다.

func reduce<Result>(
     into initialResult: Result,
     _ updateAccumulatingResult: (inout Result, Self.Element) throws -> ()
 ) rethrows -> Result
  • inout
    : inout 매개변수를 사용하여 초깃값에 직접 연산을 실행하게 됨
  • updateAccumulatingResult
    • inout
      : 모든 순회가 끝나면 리듀스의 최종 결괏값이 됨
      리듀스 메서드의 initialResult 매개변수를 이용해 전달받은 초깃값 또는 이전에 실행된 클로저 때문에 변경되어 있는 결괏값
    • Element
      : 리듀스 메서드가 순환하는 컨테이너의 요소
let letters = "abracadabra"

let letterCount = letters.reduce(into: [:]) { counts, letter in
    counts[letter, default: 0] += 1 //이전 값에 새로운 값을 할당해줘야 함
}
// letterCount == ["a": 5, "b": 2, "r": 2, "c": 1, "d": 1]
let numbers: [Int] = [1,2,3]

var subtractFromThree: Int = numbers.reduce(into: 3,{
    return $0 -= $1 //이전 값에 새로운 값을 할당해줘야 함
})
// subtractFromThree == -3

참고 스위프트 프로그래밍 3판