개발하는 뚝딱이

[swift] 맵, 필터, 리듀스 본문

swift

[swift] 맵, 필터, 리듀스

개발자뚝딱이 2020. 5. 13. 07:42

 

책 <스위프트 프로그래밍 3판 ;야곰 지음> 을 정리한 글입니다.

 


 

 

고차함수

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

- 스위프트에 유용한 대표적인 고차함수 ; 맵, 필터, 리듀스 등이 있음

 

 

1. 맵

- map

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

- 맵은 배열, 딕셔너리, 세트, 옵셔널 등에서 사용 가능

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

- 기존 데이터를 변형하는데 많은 사용이 됨

 

- for in 구문과 맵 메서드 사용 비교

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

var doubleNumbers: [Int] = [Int]()
var strings: [String] = [String]()

// for 구문 사용
for number in numbers {
    doubleNumbers.append(number*2)
    strings.append("\(number)")
}

print(doubleNumbers) // 0, 2, 4, 6, 8
print(strings) // ["0", "1", "2", "3", "4"]


// map 메서드 사용
doubleNumbers = numbers.map({ (number: Int) -> Int in
    return number*2
})
strings = numbers.map({ (number: Int) -> String in
    return "\(number)"
})

print(doubleNumbers) // 0, 2, 4, 6, 8
print(strings) // ["0", "1", "2", "3", "4"]

 

- 클로저 표현의 간략화

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

// 기본 클로저 표현식 사용
var doubleNumbers = numbers.map ({ (number: Int) -> Int in
    return number * 2
})

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

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

// 후행 클로저 사용
doubleNumbers = numbers.map { $0 * 2 }
print(doubleNumbers)

 

- 클로저의 반복 사용

let evenNumbers: [Int] = [0, 2, 4, 6, 8]
let oddNumbers: [Int] = [0, 1, 3, 5, 7]
let multiplyTwo: (Int) -> Int = { $0 * 2 }

let doubleEvenNumbers = evenNumbers.map(multiplyTwo)
print(doubleEvenNumbers) // [0, 4, 8, 12, 16]

let doubleOddNumbers = oddNumbers.map(multiplyTwo)
print(doubleOddNumbers) // [0, 2, 6, 10, 14]

 

- 다양한 컨테이너 타입에서의 맵의 활용

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

var keys: [String] = alphabetDictionary.map { (tuple: (String, String)) -> String in
    return tuple.0
}

keys = alphabetDictionary.map{ $0.0 }

let values: [String] = alphabetDictionary.map{ $0.1 }
print(keys) // ["b", "a"]
print(values) // ["B", "A"]

var numberSet: Set<Int> = [1, 2, 3, 4, 5]
let resultSet = numberSet.map{ $0 * 2 }
print(resultSet) // [2, 4, 6, 8, 10]

let operationalInt: Int? = 3
let resultInt: Int? = operationalInt.map { $0 * 2 }
print(resultInt) // Optional(6) - 타입캐스팅의 이유로 경고 발생

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

 

 

2. 필터

- filter

- 특정 조건에 맞게 값을 걸러서 추출하는 역할

- filter 함수의 매개변수로 전달되는 함수의 반환 타입은 Bool

- 해당 콘텐츠의 값을 갖고 새로운 컨테이너에 포함될 항목이라고 판단하면 true, 포함하지 않으려면 false를 반환

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

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

print(evenNumbers) // [0, 2, 4]

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

 

- 맵과 필터 메서드의 연계 사용

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

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

let evenNumbers: [Int] = mappedNumbers.filter { $0%2 == 0 }
print(evenNumbers) // [4, 6, 8]

// mappedNumbers를 굳이 여러 번 사용할 필요가 없다면 메서드를 체인처럼 연결하여 사용할 수 있음
let oddNumbers: [Int] = numbers.map{ $0+3 }.filter{ $0%2 == 1}
print(oddNumbers) // [3, 5, 7]

 

3. 리듀스

- reduce

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

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

- 스위프트의 리듀스는 두 가지 형태

 

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

- initialResult이라는 이름의 매개변수로 전달되는 값을 통해 초깃값을 지정해줄 수 있음

- nextPartialResult라는 이름의 매개변수로 클로저를 전달받음

- nextPartialResult 클로저의 첫 번째 매개변수는 리듀스 메서드의 initialResult 매개변수를 통해 전달받은 초깃값 또는 이전 클로저의 결괏값이고, 모든 순회가 끝나면 리듀스의 최종 결괏값이 됨

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

 

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

- 대신 inout 매개변수를 사용하여 초깃값에 직접 연산을 실행하게 됨

- updateAccumulatingResult 매개변수로 전달받는 클로저의 매개변수 중 첫 번째 매개변수를 inout 매개변수로 사용

- updateAccumulatingResult 클로저의 첫 번째 매개변수는 리듀스 메서드의 initialResult 매개변수를 이용해 전달받은 초깃값 또는 이전에 실행된 클로저 때문에 변경되어 있는 결과값임

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


- 첫 번째 형태인 reduce(_:_:) 메서드의 사용

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

// 초깃값이 0이고 정수 배열의 모든 값을 더합니다.
var 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

// 초깃값이 0이고 정수 배열의 모든 값을 뺍니다.
let subtract: Int = numbers.reduce(0) { (result: Int, next: Int) -> Int in
    print("\(result) - \(next)")
    // 0 - 1
    // -1 - 2
    // -3 - 3
    return result - next
}
print(subtract) // -6

// 초깃값이 3이고 정수 배열의 모든 값을 더합니다.
let sumFromThree: Int = numbers.reduce(3) {
    print("\($0) + \($1)")
    // 3 + 1
    // 4 + 2
    // 6 + 3
    return $0 + $1
}
print(sumFromThree) // 9

var subtractFromThree: Int = numbers.reduce(3) {
    print("\($0) - \($1)")
    // 3 - 1
    // 2 - 2
    // 0 - 3
    return $0 - $1
}
print(subtractFromThree) // -3

// 문자열 배열을 reduce(_:_:) 메서드를 이용해 연결시킵니다.
let names: [String] = ["Chope", "Jay", "Joker", "Nova"]

let reduceNames: String = names.reduce("friend:") {
    return $0 + " " + $1
}

print(reduceNames) // friend: Chope Jay Joker Nova

 

- 두 번째 형태인 reduce(into:_:) 메서드의 사용

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

// 초깃값이 0이고 정수 배열의 모든 값을 더합니다.
// 첫 번째 리듀스 형태와 달리 클로저의 값을 반환하지 않고 내부에서
// 직접 이전 값을 변경한다는 점이 다릅니다.

var 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

// 초깃값이 3이고 정수 배열의 모든 값을 뺍니다.
// 첫 번째 리듀스 형태와 달리 클로저의 값을 반환하지 않고 내부에서
// 직접 이전 값을 변경한다는 점이 다릅니다.
var subtractFromThree = numbers.reduce(into: 3, {
    print("\($0) - \($1)")
    // 3 - 1
    // 2 - 2
    // 0 - 3
    $0 -= $1
})
print(subtractFromThree) // -3

// 첫 번째 리듀스 형태와 다르기 때문에 다른 컨테이너 값을 변경하여 넣어줄 수도 있습니다.
// 맵이나 필터와 유사한 형태로 사용 가능합니다.

// 홀수는 걸러내고 짝수만 두 배로 변경하여 초깃값인 [1, 2, 3] 배열에 직접 연산합니다.
var doubledNumbers: [Int] = numbers.reduce(into: [1, 2]) { (result: inout [Int], next: Int) in
    print("result: \(result) next: \(next)")
    
    guard next%2 == 0 else { return }
    
    print("\(result) append \(next)")
    // [1, 2] append 2
    
    result.append(next * 2)
}

print(doubledNumbers) // [1, 2, 4]


// 필터와 맵을 사용하는 모습
doubledNumbers = [1, 2] + numbers.filter { $0.isMultiple(of: 2) }.map { $0 * 2 }
print(doubledNumbers) // [1, 2, 4]


// 이름을 모두 대문자로 변환하여 초깃값인 빈 배열에 직접 연산합니다.
let names: [String] = ["Chope", "Jay", "Joker", "Nova"]

var upperCaseNames: [String]
upperCaseNames = names.reduce(into: [], {
    $0.append($1.uppercased())
})

print(upperCaseNames) // ["CHOPE", "JAY", "JOKER", "NOVA"]

'swift' 카테고리의 다른 글

[swift] 모나드  (0) 2020.06.23
[swift] 타입캐스팅  (0) 2020.05.17
[swift] 옵셔널 체이닝과 빠른 종료  (0) 2020.05.08
[swift] 클로저  (0) 2020.05.07
[swift] 접근제어  (0) 2020.05.02