예거's Bicycle for the mind

[Swift] 고차함수 map, compactMap 의 차이와 활용법 본문

iOS & Swift

[Swift] 고차함수 map, compactMap 의 차이와 활용법

유예거 2021. 10. 10. 06:24

스위프트엔 고차함수(Higer-order function)라는 게 있다.

 

엥? 고차함수??

고등학교 때 배웠던 2차, 3차, 4차 함수 처럼, 차수가 2 이상인 함수를 말하는 건가 싶었다.

 

고등학생 때 배웠던 고차함수

 

고차함수가 왜 갑자기 스위프트에 등장하나 싶었는데, 그때의 그 그래프나 방정식을 풀기 위해서가 아니었다.

 

위 이미지의 고차함수들을 스위프트의 용어를 사용해서 나름대로 해석해보겠다.

먼저, y 는 리턴값으로 이해할 수 있다. 어떤 계산을 거쳐서, 최종적으로 나오는 값이니까.

 

그리고 x 는 파라미터로 볼 수 있다. 어떤 값을 집어넣느냐에 따라, 리턴값에 영향을 주니까.

a, b, c 등의 '계수'는 constant, 즉 상수로 볼 수 있다. 스위프트에서 let 으로 선언하는 값들 말이다. 변하지 않는 숫자라고 가정한다.

 

1차 함수의 경우는 간단하다.

파라미터인 x 가 변하면, 곧 리턴값인 y 도 변한다. 따라서 y 값도 예측하기가 쉽다.

 

근데 1차 함수인 y = ax + b 모양에서, 상수였던 a 자리에 ax + b 를 집어넣어 보자. 대신 b 는 c 로 바꾼다.

그러면 y = (ax + b)x + c 가 된다. 전형적인 2차 함수의 모습이다.

 

리턴값 y 에 영향을 주는 파라미터 x 가 2개가 됐다. 이제는 리턴값 y 가 나오는 패턴도 일차함수 마냥 단순하지가 않다.

요동치는 곡선이 된다. 이런 식으로 함수 안에 함수가 또 들어가있는 경우를, 고차함수라고 한다.

 

스위프트에서 함수는 일급객체(first-class citizen)라서

전달인자(Argument), 변수, 상수 등으로 저장/전달이 가능하다. 심지어 함수의 리턴을 또 다른 함수 형태로도 만들 수 있다.

(다른 함수를 전달인자로 받거나, 함수 실행의 결과로 함수를 반환하는 함수)

 

이번 글에서는 스위프트 표준 라이브러리에서 제공하는 고차함수 중, mapcompactMap 에 대해 공부해보자.

우선 Xcode 에서 map 과 compactMap 의 정의, 설명은 다음과 같다.

 

map

Returns an array containing the result of mapping the given closure over the sequence's elemetns.

 

시퀀스의 요소에 주어진 클로저를 매핑한 결과가 담긴 배열을 반환한다.

 

map 정의


compactMap

Returns an array containing the non-nil results of calling the given transformation with each element of this sequence.

 

시퀀스의 각 요소에 변형을 준 뒤, nil 이 아닌 결과가 담긴 배열을 반환한다.

 

compactMap 정의

 

둘 다, map 이름이 달린 만큼, 배열에 어떤 '함수'를 적용한 뒤, 다시 새로운 배열로 반환하는데

가장 큰 차이는 compactMapnil 이 아닌 값들만 선택적으로 반환한다는 것이다.

 

예시를 만들어보자.

 

// 다양한 String 을 요소로 같는 배열을 만들었다. "1"과 "5"는 Int 로 바꿀 수 있다.
let test: [String] = ["1", "/2/", "three", "5", "1s"]

 

나는 test 라는 [String] 의 각 요소들(each element)중에서 Int 로 변형할 수 있는 요소들만 가져오고 싶다.

 

참고로, Int(값) 의 리턴 타입은 Int? 이다. 즉, 리턴값으로 Int 를 반환할 수도, 만약 Int 로 바꿀 수가 없다면 nil 이 반환된다.

 

let stringOfInt = "456"
print(Int(stringOfInt))
// Optional(456)

let stringOfDouble = "0.01"
print(Int(stringOfDouble))
// nil

 

위 예시에서, Int(값) 의 리턴 타입이 Int? 이기 때문에, 문자열 "456" 이 옵셔널이 씌워진 상태로(wrapped) 나오는 것을 확인할 수 있다.

 

자 이제, 다시 map, compactMap 의 예시로 돌아가보자.

 

let test: [String] = ["1", "/2/", "three", "5", "1s"]

let mapTest = test.map { Int($0) }
print(mapTest)
// [Optional(1), nil, nil, Optional(5), nil]
// -> nil 이 반환되더라도, 모조리 가져옴. 또한 정상적으로 변형된 값도 Optional 이 달려있음

let compactMapTest = test.compactMap { Int($0) }
print(compactMapTest)
// [1, 5]
// -> nil 은 가져오질 않음. 그리고 변형된 값도 Optional 을 벗긴 채로 가져옴.

let filterTest = test.filter { Int($0) != nil }
print(filterTest)
// ["1", "5"]
// nil 은 가져오지 않지만, Int 값으로 변경해서 가져오는 게 아니라, 필터를 통과한 원본을 반환해줌.

 

map 을 적용한 결과는 어떤가?

 

숫자로 바뀌었더라도, 옵셔널이 씌워진 상태로 들어왔다.

즉, 나중에 저 값을 쓰려면 unwrapping 을 다시 해줘야 한다는 말이다. (귀찮아지겠지...)

 

반면 compactMap 을 적용한 결과를 보면, 내가 작성해준 클로저 내부의 변형(the given transformation)을 적용하고

non-nil results 들만 배열로 다시 반환된 것을 볼 수 있다. 심지어 옵셔널도 씌워져 있지 않다.

 

왜? nil 나오는 애들은 전부 빼버렸으니까, 확실하게 non-nil 인 요소들이라서 옵셔널이 벗겨져서 나온 것이다.

 

그리고 마지막 예시인 filter 는 참고용으로 넣은 건데

filter 고차함수는 조건을 만족하는 값을 반환해준다. 이 고차함수도 아주 유용하다.

 

나는 { Int($0) != nil } 이라는 조건을 걸었다. 즉, Int() 로 변경해봤을 때 nil 이 아닌 애들만 리턴해달라는 요청이다.

 

결과는 ["1", "5"] 가 나왔다.

숫자만 잘 뽑아오긴 했지만, Int 값이 아니라 여전히 원본 그대로인 String 타입으로 리턴됐다는 점을 확인하면 된다.

 

어떻게 활용할 수 있을까?

아직 부족한 스위프트 실력이지만

옵셔널은 최대한 없애거나, 안전하게 unwrapping 해서, non-optional 상태로 사용하는 게 좋다고 생각한다.

 

그런 면에서, 옵셔널을 생산하는 map 보다는 nil 값을 차단하고, 옵셔널이 벗겨진 값만 내놓는 compactMap 이 좀 더 매력적으로 보이긴 한다. 용도도 비슷하고.

 

나중에 좀 더 고급진 예시와 활용법을 다시 정리해봐야겠다.

Comments