[Swift] 고차함수 - map, filter, reduce (함수 컴비네이터)

Bibi·2022년 1월 20일

[Swift] 고차함수 - map, filter, reduce (함수 컴비네이터)

스위프트 프로그래밍 3판 (야곰) 을 보고 정리한 글입니다.

고차함수 : 매개변수로 함수를 갖는 함수.

특히 컬렉션(배열/리스트..등)을 탐색/비교/찾아서 정리하는 기능을 자주 사용한다.

대표적인 고차함수가 map, filter, reduce 이다.

map, filter, reduce

고차함수 왜 쓰는가?

함수형 프로그래밍의 장점을 간편하게 이용할 수 있기 때문.

  • for-in 구문에 비해 코드가 간결해짐
  • 코드 재사용, 컴파일러 최적화 측면에서 성능 차이
  • 멀티스레드 환경에서 부작용을 방지함

맵 map : 데이터 변형

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

즉, 기존 데이터를 변형하는 데 사용한다.

컨테이너가 담은 각각의 값을, 매개변수로 받은 함수에 적용한 후, 다시 컨테이너에 포장해 반환.

이 때 기존 컨테이너 값에 영향 없이, 새 컨테이너를 만들어 반환한다.

  • 사용대상 : 배열, 댁셔너리, 세트, 옵셔널 등에 사용 가능
  • 사용조건 : Sequence, Collection 프로토콜을 따르는 타입

선언

func map<T>(_ transform: @escaping (Self.Output) -> T) -> Publishers.Map<Self, T>

Array에서 map 활용

let numbers = [1, 2, 3]

var doubledNumbers = Array<Int>()

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

// map
doubledNumbers = numbers.map({ (number: Int) -> Int in
       return number * 2
}) // 기본 클로저
doubledNumbers = numbers.map({ return $0 * 2 }) // 파라미터 타입, 리턴타입 생략
doubledNumbers = numbers.map({ $0 * 2 }) // return 키워드 생략
doubledNumbers = numbers.map { $0 * 2 } // 후행 클로저 사용

let multiplyTwo: (Int) -> Int = { $0 * 2 }
doubledNumbers = numbers.map(multiplyTwo) // 재사용성을 위해 클로저를 상수로 선언

Dictionary에서 map 활용

let alphabetDict: [String : String] = ["a":"A", "b":"B"]

var keys = Array<String>()

keys = alphabetDict.map { (tuple: (String, String)) -> String in
    return tuple.0 // tuple의 0번째 = key
}

keys = alphabetDict.map { $0.0 } // ["a", "b"]

var values = Array<String>()
values = alphabetDict.map { $0.1 } // ["A", "B"]

Set 에서 map 활용

var numberSet: Set<Int> = [1, 2, 3]

let resultSet = numberSet.map{ $0 * 2 } // [2, 4, 6]

범위 표현에 map 활용

let range = 0...3 // CountableClosedRange
let resultRange: [Int] = range.map{ $0 * 2 } // [0, 2, 4, 6]

필터 filter

컨테이너 값을 특정 조건에 맞게 걸러내는 고차함수.

  • filter의 매개변수로 전달되는 클로저의 리턴타입은 Bool이어야 한다.

선언

func filter(_ isIncluded: @escaping (Self.Output) -> Bool) -> Publishers.Filter<Self>

배열에 filter 활용

let numbers = [1, 2, 3]

let evenNumbers: [Int] = numbers.filter { (number: Int) -> Bool in
    return number % 2 == 0
}
print(evenNumbers) // [2]

let oddNumbers: [Int] = numbers.filter { $0 % 2 == 1}
print(oddNumbers) // [1, 3]

리듀스 reduce

: 컨테이너 내의 모든 값을 하나로 합하는 기능.

전달인자로 받은 클로저의 연산 결과로 합한다.

상황에 따라 리듀스를 맵과 비슷하게 쓸 수 있다.

선언 1 : reduce(_:_:)

public func reduce<T>(_ initialResult: T, _ nextPartialResult: (Result, Element) throew -> Result) rethrows -> Result

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

  • initialResult : 초기값
  • nextPartialResult : 클로저. 두 개의 매개변수를 받는다
    • 클로저의 첫 번째 매개변수(Result) : initialResult로 전달받은 초기값 또는 이전 클로저의 결과값. 순회가 끝났을 때 리듀스의 최종 결과값이 된다
    • 클로저의 두 번째 매개변수(Element) : 리듀스 메서드가 순환하는 컨테이너의 요소

reduce(_:_:) 예제

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

// 초기값 0, numbers의 모든 값을 더함
let sum: Int = numbers.reduce(0, {(result: Int, next: Int) -> Int in
		print("\(result) + \(next)") 
    // 0 + 1 
    // 1 + 2
		// 3 + 3
    return result + next
})
print(sum) // 6

// 초기값 3, numbers의 모든 값을 더함
let sumFromThree: Int = numbers.reduce(3) {
  print("\($0) + \($1)")
  // 3 + 1
  // 4 + 2
  // 6 + 3
  return $0 + $1
} 
print(sumFromThree) // 9

// reduce(_:_:) 로 문자열 배열을 연결하기
let names: [String] = ["Tom", "Jerry"]
let reducedNames: String = names.reduce("They are ") {
    return $0 + " and " + $1
}
print(reducedNames) // "They are Tom and Jerry"

선언 2 : reduce(into:_:)

public func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) throws -> ()) rethrows -> Result

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

return 없이, inout 매개변수를 사용해 초기값에 직접 연산을 실행

  • initialResult : 초기값
  • updateAccumulatingResult : 클로저. 두 개의 매개변수를 받는다
    • 클로저의 첫 번째 매개변수(inout Result) : inout 매개변수 - initialResult로 전달받은 초기값 또는 이전 클로저에 의해 변경된 결과값. 순회가 끝났을 때 리듀스의 최종 결과값이 된다
    • 클로저의 두 번째 매개변수(Element) : 리듀스 메서드가 순환하는 컨테이너의 요소

reduce(into:_:) 예제

let numbers = [1, 2, 3]

// 초기값이 0이고, numbers의 모든 값을 더한다
// reduce(_:_:)과의 차이 : 클로저 값의 return 없이, 클로저 내부에서 직접 이전 값을 변경함
sum = numbers.reduce(into: 0, { (result: inout Int, next: Int) in
		print("\(result) + \(next)")
		// 0 + 1
		// 1 + 2
		// 3 + 3
		result += next
})

print(sum) // 6

reduce(::)와 달리 다른 컨테이너에 값을 변경해 넣어줄 수 있다.
맵, 필터와 유사하게 사용이 가능하다.

// 이름을 모두 대문자로 변환해 초기값인 빈 배열에 직접 연산
let names: [String] = ["bibi", "coco", "ruru"]
var uppercasedNames: [String]
uppercasedNames = names.reduce(into: [], {
    print("$n : \($0), $n+1 : \($1)")
//    $n : [], $n+1 : bibi
//    $n : ["BIBI"], $n+1 : coco
//    $n : ["BIBI", "COCO"], $n+1 : ruru
    $0.append($1.uppercased())
})
print(uppercasedNames) // ["BIBI", "COCO", "RURU"]

// 맵을 사용한 모습
uppercasedNames = names.map { $0.uppercased() }

예제 - 맵과 필터 함께 사용하기

let numbers = [0, 1, 2, 3, 4, 5]

let mappedNumbers: [Int] = numbers.map { $0 + 3 } // [0, 4, 5, 6, 7, 8]
let evenNumbers: [Int] = mappedNumbers.filter { (number: Int) -> Bool in
		return number % 2 == 0
}
print(evenNumbers) // [4, 6, 8]

// mappedNumbers 없이 체이닝도 가능
let evenNumbers: [Int] = numbers.map{ $0 + 3 }.filter{ $0 % 2 == 0}

예제 - 맵, 필터, 리듀스 함께 사용하기

let numbers = [1, 2, 3, 4, 5, 6, 7]

// 짝수를 걸러내고, 3을 곱한 뒤, 모든 값 더하기
var result: Int = numbers.filter{ $0.isMultiple(of: 2) }.map{ $0 * 3 }.reduce(0){ $0 + $1 } // 2, 4, 6 -> 6, 12, 18 -> 36
print(result) // 36

0개의 댓글