GGURUPiOS

Swift 공식 문서 정리 - (28) 고차함수 (map, filter, reduce, flatMap) 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (28) 고차함수 (map, filter, reduce, flatMap)

꾸럽 2023. 5. 14. 15:56

고차함수

스위프트는 함수를 일급 객체로 취급하기 때문에 함수를 다른 함수의 전달 인자로 사용할 수 있음

고차함수란? 파라미터로 함수를 갖는 함수를 고차함수라고 함

Map, Filter, Reduce 에 대해 알아보자

Map ( 맵 )

맵은 자신을 호출할 때 파라미터로 전달된 함수를 실행하여 그 결과를 다시 반환 하는 함수임

컬렉션 프로토콜을 따르는 타입에서 사용 가능 ( 배열 , 딕셔너리 , 세트 등 )

파라미터를 통해 받은 함수에 적용한 후 다시 반환 함

→ 기존 데이터를 변형 하는데 많이 사용함

map 메서드의 사용법은 For-in 구문과 비슷함

코드의 재사용 측면이나 컴파일러 최적화 측면에서 본다면 성능 ㅊ ㅏ이가 있음

다중 스레드 환경에서 대상 컨테이너의 값이 스레드에서 변경되는 시점에 다른 스레드에서 동시에 변겨오디려고 할 때 발생하는 부작용 방지 가능

for-in 구문과 map 을 비교해 보자

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

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

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

// map 을 써보자

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

위에서 처럼 Map 메소드를 사용하면 For-in 구문을 사용하기 위해 빈 배열을 안마들어도 되고, 배열의 append 연산을 수행하기 위한 시간도 필요 없음

클로저 표현식을 사용해 표현을 더 간략화 할 수 있음

let nmubers = [0 ,1, 2, 3, 4]
var doublenumbers = numbers.map( {number: Int) -> Int in
	return number * 2
})

// 파라미터 및 반환 타입 생략 
doubleumbers = numbers.map({ return $0 * 2 })

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

// 트레일링 클로저 사용 
doubleNumbers = numbers.map { $0 * 2 }

map 은 배열에서 사용할 수 있는 것은 아니며, 여러 컨테이너 타입에 모두 적용 가능 함

Filter ( 필터 )

필터는 말 그대로 값을 걸러서 추출하는 역할을 하는 고차함수임

맵처럼 기존 콘텐츠를 변형하는 것이 아닌, 특정 조건에 맞게 걸러내는 역할 임

filter 함수의 파라미터로 전달되는 함수의 반환 타입은 Bool 임

Bool 을 가지고 걸러낼것인지, 포함할 것인지 판단 함

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

let evenNumbers: [Int] = numbers.filter { (number: Int) -> Bool in
	return number % 2 == 0 
}
// 역시 클로저 생략이 가능 아래와 같이 작성가능
let oddNumbers: [Int] = numbers.filter { $0 % 2 != 0 }

map 과 filter 를 같이 쓰는 예제

let numbers: [0, 1, 2, 3, 4, 5]
let mappedNumbers: [Int] = numbers.map{ $0 + 3 }
let evenNumbers: [Int] = mappedNumbers.filter{ $0 % 2 == 0 }

// 만약 상수 mappedNumbers 가 변형하는 역할 외에 다른곳에서 안쓰인다면 체인처럼 연결해 사용 가능

let oddNumbers: [Int] = number.map{ $0 + 3}.filter{ $0 % 2 != }

Reduce ( 리듀스 )

리듀스 기능은 사실 Combine 이라고 불려야 마땅한 기능임

컨테이너 내부의 콘텐츠를 하나로 합쳐주는 기능을 수행

만약 정수 배열이라면 정수 배열의 모든 값을 전달인자로 전달받은 함수의 연산 결과로 합쳐주고,

문자열 배열이라면 문자열을 하나로 합쳐줌

initial이라는 이름의 파라미터로 전달되는 값을 통해 초깃값을 지정해 줄 수 있음

예제를 보며 알아보자

let numbers = [1, 2 ,3]

// 초깃값이 0이고 정수 배열의 모든 값을 더하는 reduce 함수 구현
var sum: Int = numbers.reduce(0, { first: Int, second: Int) -> Int in 
	print("\\(first) + \\(second)")
// 0 + 1
// 1 + 2
// 3 + 3
	return first + second
})

print(sum) // 6

// 초깃값이 0 이고 정수 배열의 모든 값을 빼는 reduce 함수 구현
var subtract: Int = numbers.reduce(0, { first: Int, second: Int) -> Int in 
	print("\\(first) - \\(second)")
// 0 + 1
// 1 + 2
// 3 + 3
	return first - second
})

print(subtract) // -6

// 초깃값이 3이고 정수 배열의 모든 값을 더하는 reduce 함수 구현
let sumFromThree: Int = numbers.reduce(3) {
	return $0 + $1
}

print(sumFromThree) // 9

이런식으로 사용이 가능함

맵, 필터, 리듀스 메서드를 다 써보자

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

// 짝수를 걸러내어 각 값에 3을 곱해준 후 모든 값을 더함
var result: Int = numbers.filter { $0 % 2 == 0 }.map { $0 * 3 }. reduce(0){ $0 + $1 }
print(result) // 36


flatMap 을 배우기전에 개념을 더 배워보자

모나드

모나드는 특정한 상태로 값을 포장하는 것에서 시작함

스위프트에서는 이를 옵셔널이라는 형태로 구현했는데 값이 있을지 없을지 모르는 상태 속에 포장하는 것임

함수객체와 모나드는 특정 기능이 아닌 디자인 패턴 혹은 자료구조라고 할 수 있음

컨텍스트

컨텍스트의 사전적 정의를 보면 맥락, 전후사정 등임

이 파트에서 컨텍스트는 ‘컨텐츠 를 담고있는 무엇인가’ 를 뜻 함

( 물컵에서 물은 컨텐츠, 물컵은 컨텍스트 )

옵셔널은 열거형으로 구현되어 있어서 열거형 케이스의 연관 값을 통해 인스턴스 안에 연관 값을 가지는 형태임

값이 없다면 열거형의 .none 케이스로,

값이 있다면 .some(value) 케이스로 값을 지니게 됨

옵셔널 값을 추출하는 것은 .some(value) 케이스의 연관값을 가져오는 것과 같음

2라는 숫자를 옵셔널로 둘러싸면, 컨텍스트 안에 2라는 콘텐츠가 들어가는 모양새임

옵셔널은 some과 none이라는 두 가지의 컨텍스트를 가짐

func addThree(_ num: Int) -> Int {
	return num + 3
}

/*
여기서 함수의 파라미터에 순수한 값 2를 전달하면 정상 작동하고
옵셔널(2)를 사용하면 오류가 난다
컨텍스트(무언가에 둘러쌓인)(여기서는 옵셔널타입의 .some 케이스)가 전달되었기 때문
*/

함수 객체

앞서 배운 Map 은 컨테이너 값을 변형사킬 수 있는 고차함수

그리고 옵셔널은 컨테이너(컨텍스트가 일종의 컨테이너 역할)와 값을 가지기 때문에 맵 함수를 사용할 수 있음

Optional(2).map(addThree) 가 가능하다.

따라서 함수가 없어도 클로저를 사용할 수도 있음

함수 객체란 맵을 적용할 수 있는 컨테이너 타입

→ Array, Ditctionary, Set 등등 스위프트의 많은 컬렉션 타입은 함수객체임

맵의 내부 동작

  1. 맵이 함수를 인자로 받음
  2. 함수 객체에 맵이 전달 받은 함수를 적용
  3. 새로운 함수객체를 반환

Optional(2).map(addThree)의 코드 동작

  1. 컨텍스트로부터 값 추출 2
  2. 전달받은 함수 적용 2 +3 =5
  3. 결괏값을 다시 컨텍스트에 담아 반환 5

Optional.none.map(addThree)의 코드 동작

  1. 컨텍스트에 값이 없음
  2. 함수 적용 안함
  3. 결과 적으로 아무것도 하지 않음 ( 빈 컨텍스트로 다시 반환 )

모나드

모나드는 함수객체의 일종임

함수객체는 맵 함수를 지원하는 컨테이너 타입 이었음

모나드는 거기에 더 나아가 값이 있을지 없을지 모르는 상태를 추가함

→ 모나드는 값이 있을 수도 있고 없을 수도 있는 컨텍스트를 가지는 함수객체 타입임

함수객체는 포장된 값에 함수를 적용할 수 있었음

그래서 모나드도 컨텍스트에 포장된 값을 처리하여 컨텍스트에 포장된 값을 다시 반환하는 함수를 적용 가능

→ 스위프트에서는 이와같은 기능을 수행하기 위해 Flatmap이란 메소드가 있음

Flatmap

플랫맵은 포장된 값을 받아서 값이 있으면 포장을 풀어서 값을 처리한 후 포장된 값을 반환,

값이 없으면 값이 없는 대로 다시 포장하여 반환 함

맵과 같이 플랫맵도 함수를 파라미터로 받고, 옵셔널은 모나드이므로 플랫맵을 사용할 수 있음

아래와 같이 짝수면 2를 곱해서 반환하고 짝수가 아니면 nil을 반환하는 함수가 있을 때, Optional(3)의 플랫맵에 함수를 전달했을 경우 결과를 살펴보자

func doubleEven(_ num: Int) -> Int? {
	if num % 2 == 0 {
		return num * 2
	}
	return nil
}

Optional(3).flatMap(doubleEven)
// nil == Optional.none 

동작 순서

  1. 컨텍스트로부터 값 추출
  2. 추출한 값을 doubledEven 함수에 전달
  3. 빈 컨텍스트 반환

Otpinoal.none.flatMap(doubleEven) 코드 동작 순서

  1. 빈 컨텍스트
  2. 플랫맵은 아무것도 하지 않음
  3. 다시 빈 컨텍스트 반환

플랫맵과 맵과의 차이점은 내부의 값을 알아서 더 추출해 준다는 것임

플랫맵은 내부에 포장된 값도 추출해낼 수 있음

let optionalArr: [Int?] = [1, 2, nil, 5]

let mappedArr: [Int?] = optionalArr.map{ $0 }
let flatmappedArr: [Int] = optionalArr.flatMap{ $0 }

print(mappedArr) // [Optional(1), Optional(2), nil, Optional(5)]
print(flatmappedArr) // [1, 2, 5]

map 메서드를 사용한 결과는 Array 컨테이너 내부의 값이 타입이나 형태가 어찌 되었든

Array 내부에 값이 있으면 그 값을 클로저의 코드에만 실행하고 결과를 다시 담아서 내보낸다

그러나 flatmap을 통해 클로저를 실행하면 알아서 내부 컨테이너 값을 추출함

플랫맵은 내부의 값을 1차원 적으로 펼쳐놓는 작업도 수행함 (Flatten)

→ 옵셔널에 관련된 여러 컨테이너의 값을 연달아 처리할 때, 바인딩을 통해 체인형식으로 사용할 수 있음