210423 Fri

  • 스위프트는 함수를 일급 객체로 취급함

→ 함수를 다른 함수의 전달인자로 사용할 수 있음

  • 매개변수로 함수를 갖는 함수를 고차함수라고 부름
  • 대표적인 고차함수: 맵, 필터, 리듀스 등

출처: 스위프트 프로그래밍(3판) 3부 15장 by 야곰

1. 첫 번째 학습 내용: Map (맵)

  • Map은 자신을 호출할 때 매개변수로 전달된 함수를 실행하여 그 결과를 다시 반환해주는 함수
  • 스위프트에서 맵은 배열, 딕셔너리, 세트, 옵셔널 등에서 사용할 수 있음
  • 맵을 사용하면 컨테이너가 담고 있던 각각의 값을 매개변수를 통해 받은 함수에 적용한 후 다시 컨테이너에 포장하여 반환함
  • 기존 컨테이너의 값은 변경되지 않고 새로운 컨테이너가 생성되어 반환됨

→ 그래서 맵은 기존데이터를 transform(변형)하는 데 많이 사용함

Q. map을 왜 쓰는가?

  1. 코드의 재사용 측면이나 컴파일러 최적화 측면에서 본다면 for-in 구문과 성능 차이가 있음
  2. 다중 스레드 환경일 때 대상 컨테이너의 값이 스레드에서 변경되는 시점에 다르 스레드에서도 동시에 값이 변경되려고 할 때 예측지 못한 결과가 발생하는 부작용을 방지할 수도 있음

for 구문 사용

import Foundation

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

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

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

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

map 메서드 사용

import Foundation

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

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

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

strings = numbers.map({ (number: Int) -> String in
    return "\(number)"
})

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

클로저 표현의 간략화

import Foundation

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

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

// 매개변수 및 반환 타입 생략
doubledNumbers = numbers.map({ return $0 * 2 })
print(doubledNumbers) // [0, 2, 4, 6, 8]

// 반환 키워드 생략
doubledNumbers = numbers.map({ $0 * 2 })
print(doubledNumbers) // [0, 2, 4, 6, 8]

// 후행 클로저 사용
doubledNumbers = numbers.map{ $0 * 2 }
print(doubledNumbers) // [0, 2, 4, 6, 8]

클로저의 반복 사용

import Foundation

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

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

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

2. 두 번째 학습 내용: 필터 (Filter)

  • Filter는 말 그대로 컨테이너 내부의 값을 걸러서 추출하는 역할을 하는 고차함수

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

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

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

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

필터 메서드의 사용

import Foundation

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]

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

import Foundation

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

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

let evenNumbers: [Int] = mappedNumbers.filter { (number: Int) -> Bool in
    return number % 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)

  • Reduce 기능은 사실 결합(Combine)이라고 불러야 마땅한 기능임
  • 리듀스는 컨테이너 내부의 콘텐츠를 하나로 합하는 기능을 실행하는 고차함수
  • 배열이라면 배열의 모든 값을 전달인자로 전달받은 클로저의 연산 결과로 합해줌

스위프트의 리듀스는 두 가지 형태로 구현되어 있음

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

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

두 번째 리듀스 메서드는 컨테이너를 순환하며 클로저가 실행되지만 클로저가 따로 결과값을 반환하지 않는 형태. 대신 inout 매개변수를 사용하여 초깃값에 직접 연산을 실행하게 됨

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

리듀스 메서드의 사용

초기값이 0이고 정수 배열의 모든 값을 더합니다

import Foundation

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

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

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

초기값이 3이고 정수 배열의 모든 값을 뺍니다

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

 print(subtractFromThree) // -3

문자열 배열을 reduce(: :) 메서드를 이용해 연결시킵니다

import Foundation

let names: [String] = ["Sunny", "Steven", "Summer", "Kio", "Tak", "Coda"]

let reducedNames: String = names.reduce("yagom 2기 캠퍼들 : ") {
    return $0 + ", " + $1
}

print(reducedNames) // yagom 2기 캠퍼들 : , Sunny, Steven, Summer, Kio, Tak, Coda

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

초기값이 0이고 정수 배열의 모든 값을 더합니다.

첫 번째 리듀스 형태와 달리 클로저의 값을 반환하지 않고

내부에서 직접 이전 값을 변경한다는 점이 다릅니다.

import Foundation

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

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] 배열에 직접 연산합니다.


guard 문에 next.is가 뭐지??

에러가 나는데 홀.. 🤔

필터와 맵을 사용한 모습

var doubledNumbers = [1, 2] + numbers.filter{ $0.isMultiple(of: 2) }.map { $0 * 2 }
print(doubledNumbers) // [1, 2, 4]

이름을 모두 대문자로 변환하여 초기값인 빈 배열에 직접 연산

let names: [String] = ["Sunny", "Steven", "Summer", "Kio", "Tak", "Coda"]

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

print(upperCasedNames) // ["SUNNY", "STEVEN", "SUMMER", "KIO", "TAK", "CODA"]

맵을 사용한 모습

var upperCasedNames = names.map { $0.uppercased() }
print(upperCasedNames) // ["SUNNY", "STEVEN", "SUMMER", "KIO", "TAK", "CODA"]

맵, 필터, 리듀스 메서드의 연계 사용

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

// 짝수를 걸러내어 각 값에 3을 곱해준 후 모든 값을 더함

var result: Int = numbers.filter{ $0.isMultiple(of: 2) }.map{ $0 * 3 }.reduce(0){ 
$0 + $1 }
print(result) // 36

for-in 구문 사용시

let numbers: [Int] = [1, 2, 3, 4, 5, 6, 7]
var result = 0

for number in numbers {
    guard number.isMultiple(of: 2) else {
        continue
    }
    
    result += number * 3
}

print(result) // 36

맵, 필터, 리듀스의 활용

import Foundation

enum CoffeeType {
    case caffeine, decaffeination, unknown
}

struct coffeeMenu {
    let name: String
    let coffeeType: CoffeeType
    var price: UInt
}

var coffees: [coffeeMenu] = [coffeeMenu]()

coffees.append(coffeeMenu(name: "카페 라떼", coffeeType: .caffeine, price: 4000))
coffees.append(coffeeMenu(name: "따듯한 아메리카노", coffeeType: .caffeine, price: 3500))
coffees.append(coffeeMenu(name: "에티오피아 원두 커피", coffeeType: .decaffeination, price: 3500))

// 카페인이 들어가면서 가격이 4천원 이하인 커피 메뉴 찾기

var result: [coffeeMenu] = coffees.map{ coffeeMenu(name: $0.name, coffeeType: $0.coffeeType, price: $0.price) }

result = result.filter{ $0.coffeeType == .caffeine && $0.price <= 4000 }

let string: String = result.reduce("카페인이 들어가면서 가격이 4천원 이하인 커피 메뉴 ☕️") { $0 + "\n" + "\($1.name) \($1.coffeeType) \($1.price)원"}

print(string)
//카페인이 들어가면서 가격이 4천원 이하인 커피 메뉴 ☕️
//카페 라떼 caffeine 4000원
//따듯한 아메리카노 caffeine 3500원 

좋은 웹페이지 즐겨찾기