고차함수 - Map, Filter, Reduce

이원희·2021년 1월 4일
0

 🐧 Swift

목록 보기
17/32
post-thumbnail

오늘은 고차함수에 대해서 알아보자.
Swift에서 제공하는 고차함수인 Map, Filter, Reduce를 자주 사용했었는데 이참에 정리해두려 한다.

고차함수

고차함수는 다른 함수를 전달인자로 받거나 함수 실행의 결과를 함수로 반환하는 함수이다.
Swift에서 제공하는 고차함수로는 Map, Filter, Reduce가 있다.
하나씩 알아가보면서 고차함수를 이해해보자.


Map

map컨테이너 내부의 기존 데이터를 변형하여 새로운 컨테이너를 생성한다.

이렇게 말하면 무슨 말인지 잘 모르겠다.
Int형 배열을 String형 배열로 바꾸는 코드를 통해 이해해보자!

for문으로 구현해보자

let numbers: [Int] = [0, 1, 2, 3, 4]
var stringNumbers: [String] = []
for number in numbers {
	stringNumbers.append(number)
}
print(stringNumbers)

// 결과
["0", "1", "2", "3", "4"]

map을 사용해보자

let numbers: [Int] = [0, 1, 2, 3, 4]
let stringNumbers = numbers.map({(number: Int) -> String in
    return "\(number)"
})
print(stringNumbers)

// 결과
["0", "1", "2", "3", "4"]

태초에 Int 형 배열 numbers가 있었다..ㅋㅋㅋ
우리가 map으로 변형한 stringNumbers는 어떤 타입의 배열일까?
결과를 보면 String 타입임을 알 수 있다.
그렇다면 map을 통해 [Int][String]으로 변형됐다는 뜻인데 한 번 봐보자.

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

위의 코드를 봐보자.
()안에 클로저를 넘겨주고 있다.
클로저를 확인해보니 IntString으로 바꿔주는거 같다.

그렇다면 위의 map과 같은 기능을 하도록 for문을 사용해 코드를 짤 수 있을까?

그렇다면 numbers: [Int]에서 요소 하나하나를 돌면서 [String]으로 변형해주고 있구나를 알 수 있다.
그렇다면 numbersOptional이 있다면 어떻게 될까?


Optional과 Map

let numbers: [Int?] = [0, 1, 2, nil, 4]
let stringNumbers = numbers.map({(number: Int?) -> String in
    return "\(number)"
})
print(stringNumbers)

// 결과
["Optional(0)", "Optional(1)", "Optional(2)", "nil", "Optional(4)"]

nil을 넣기 위해 number의 타입을 [Int?]로 변경했다.
nil이 아닌 값은 Optional String으로 변환되지만 nil인 값은 똑같이 nil임을 확인할 수 있다.

그렇다면 nil을 없애려면 어떻게 할 수 있을까?

let numbers: [Int?] = [0, 1, 2, nil, 4]
var stringNumbers: [String] = []
for number in numbers {
    if let n = number {
        stringNumbers.append("\(n)")
    }
}

for문과 if let을 이용할 수 있지만 더 Cool한 방법이 있다!


CompactMap

let numbers: [Int?] = [0, 1, 2, nil, 4]
let stringNumbers = numbers.compactMap { number in
    return number
}
print(stringNumbers)

// 결과
[0, 1, 2, 4]

compactMap을 사용하면 nil이 제거된 결과를 확인할 수 있다.

그렇다면 2차원 배열일때는 어떨까?

let numbers: [[Int?]] = [[0, 1, 2, nil, 4], [5, 6, 7, nil, 8]]
let stringNumbers = numbers.compactMap { number in
    return number
}
print(stringNumbers)

// 결과
[[Optional(0), Optional(1), Optional(2), nil, Optional(4)], [Optional(5), Optional(6), Optional(7), nil, Optional(8)]]

2차원 배열을 유지하고 있다.
nil이 제거되지 않는 것을 확인할 수 있다.


FlatMap

let numbers: [Int?] = [0, 1, 2, nil, 4]
let stringNumbers = numbers.flatMap { number in
    return number
}
print(stringNumbers)

// 결과
[0, 1, 2, 4]

flatMap을 사용해도 nil이 제거된 결과를 확인할 수 있는데 그렇다면 2차원 배열일때는 어떨까?

let numbers: [[Int?]] = [[0, 1, 2, nil, 4], [5, 6, 7, nil, 8]]
let stringNumbers = numbers.flatMap { number in 
    return number
}
print(stringNumbers)

// 결과
[Optional(0), Optional(1), Optional(2), nil, Optional(4), Optional(5), Optional(6), Optional(7), nil, Optional(8)]

오잉...?!
2차원 배열이 1차원 배열로 변했다...!
flat은 평면 이라는 뜻으로 flatMap도 2차원 배열을 1차원 배열로 평평하게 만들어 주고 있다.

1차원 배열에서는 compactMapflatMap이 동일한 결과를 보여주고 있는데 왜 그럴까?

기존의 flatMap은 배열을

  • flatten하게 만듬
  • nil을 제거
  • 옵셔널 바인딩

하는 역할을 했다.
이중에서 "nil을 제거"하는 기능을 compactMap이 하게 되었다.

그렇다면 2차원 배열에서 nil은 어떻게 없앨 수 있을까?

let numbers: [[Int?]] = [[0, 1, 2, nil, 4], [5, 6, 7, nil, 8]]

var forStringNumbers: [[Int]] = []
for number in numbers {
    forStringNumbers.append(number.compactMap{ n in
        return n
    })
}
print(forStringNumbers)

// 결과
[[0, 1, 2, 4], [5, 6, 7, 8]]

for문과 compactMap을 조합해 2차원 배열을 유지하고 nil을 제거할 수 있다.

let numbers: [[Int?]] = [[0, 1, 2, nil, 4], [5, 6, 7, nil, 8]]
let compactFlatStringNumbers = numbers.flatMap{ $0 }.compactMap{ $0 }
print(compactFlatStringNumbers)

// 결과
[0, 1, 2, 4, 5, 6, 7, 8]

flatMapcompactMap를 조합해 nil을 제거한 1차원 배열을 얻을 수 있다.


Filter

filter컨테이너 내부의 값을 걸러서 새롤운 컨테이너로 추출한다.
새로운 컨테이너에 담아서 반환한다.

map은 기존의 data를 변형해서 새로운 컨테이너에 담아서 반환하지만
filter는 기존 data를 그대로 가져와서 담아서 반환한다.
filter 함수의 매개변수로 전달되는 함수의 반환 타입은 Bool이다.

filter 함수를 확인하기 전에for문으로 먼저 구현해보자.
numbers 배열에서 짝수만 반환하는 배열을 구해보자.

for문으로 구현해보자

let numbers = [0, 1, 2, 3, 4]
var filterNumbers: [Int] = []
for number in numbers {
	if number % 2 == 0 {
    	filterNumbers.append(number)
    }
}
// 결과
[0, 2, 4]

filter를 사용해보자

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

// 결과
[0, 2, 4]

number % 2 == 0이라면 filter된 배열에 담기게 된다.
즉, 짝수만 배열에 담겨 반환된다.


Reduce

reduce컨테이너 내부의 콘텐츠를 하나로 통합한다.
reduce를 사용할때는 초기값을 줘야한다.

numbers 배열의 요소를 다 더한 값을 구해보자.

for문으로 구현해보자

let numbers = [0, 1, 2, 3, 4]
var reduceNumber = 0
for number in numbers {
    reduceNumber = reduceNumber + number
}
print(reduceNumber)

// 결과
10

reduce를 사용해보자

let numbers = [0, 1, 2, 3, 4]
let reduceNumber = numbers.reduce(0, { (first: Int, second: Int) -> Int in
    return first + second
})
print(reduceNumber)

// 결과
10

0이라는 초기값을 주었다.


왜 사용할까?

그렇다면 왜 위와 같은 고차함수를 사용할까?
기존에 for문을 통해서도 구현할 수 있는데!

위에서 for문을 이용해 구현한 코드들의 특징이 있다.
반환할 프로퍼티들은 모두 var(변수)로 정의했다.
for문을 돌면서 적합한 요소들을 append(reduce 말고..)해주기 위해 변수로 선언했다.
고차함수를 이용해 구현한 코드들은 let(상수)로 정의되어 있다.

for문을 사용한다면 상수로 표현할 수 없지만 고차함수를 사용한다면 상수로 표현할 수 있다.

이렇게 상수로 사용한다면 Compile시 유리하다.


마무리

오늘은 고차함수에 대해서 알아봤다.
프로젝트를 진행하면서 고차함수를 자주 쓰게 되는데 굉장히 편하고, 함수의 뎁스를 줄일 수 있는 방법 중 하나라고 생각한다.
오늘 정리했으니 앞으로는 헷갈리지 않고 잘 쓸 수 있도록...!
그럼 이만👋

0개의 댓글