서론
Swift 4.1에서 flatMap이 deprecated되고 compactMap으로 역할이 분리되었습니다.
저는 한 가지 착각하고 있었습니다. "flatMap이 아예 사라지고 compactMap이 새로 생겼구나"라고 오해했는데요, flatMap은 여러 형태가 존재했고, flatMap 중 하나가 compactMap이 된거였습니다.
이번 포스팅은 pointfree의 https://www.pointfree.co/episodes/ep10-a-tale-of-two-flat-maps 영상과 스크립트를 정리한 포스팅입니다. flatMap이 어떤 형태를 제공했었고, 어떤 메서드가 compactMap이 된 것인지 알아보겠습니다.
flatMap의 세 가지 형태
flatMap은 총 세 가지 형태로 제공됩니다.
extension Array {
func flatMap<B>(_ f: @escaping (Element) -> [B]) -> [B]
}
// 예시
let nested = [[1, 2], [3, 4], [5, 6]]
let flattened = nested.flatMap { $0 } // [1, 2, 3, 4, 5, 6]
가장 기본적인 형태의 flatMap입니다. 중첩된 배열을 평탄화하는데 사용됩니다.
extension Optional {
func flatMap<B>(_ f: @escaping (Element) -> B?) -> B?
}
// 예시
let stringNumber = "42"
let result = Optional(stringNumber).flatMap(Int.init) // Optional(42)
옵셔널 체이닝을 위한 flatMap입니다. 중첩된 옵셔널을 단일 옵셔널로 만들어줍니다.
extension Array {
func flatMap<B>(_ transform: (Element) -> B?) -> [B]
}
배열의 각 요소를 변환하면서 nil을 제거하는 역할을 합니다.
이 버전의 flatMap이 오늘의 주인공입니다.
문제점
세 가지 형태 중 세 번째 flatMap은 어떤 문제가 있었을까요?
먼저 타입 시그니처의 불일치입니다.
// 1번: Array -> Array
flatMap: ((A) -> [B]) -> ([A]) -> [B]
// 2번: Optional -> Optional
flatMap: ((A) -> B?) -> (A?) -> B?
// 3번: Array + Optional -> Array (???)
flatMap: ((A) -> B?) -> ([A]) -> [B]
세 번째 flatMap은 Array와 Optional 두 가지 컨테이너 타입을 동시에 다루고 있습니다. 이는 함수형 프로그래밍 관점에선 일관성이 떨어지는 설계입니다.
두 번째는 옵셔널 처리의 모호함 때문입니다.
let numbers = [1, 2, 3]
numbers.flatMap { $0 + 1 } // [2, 3, 4]
이 코드는 컴파일러가 자동으로 반환값을 .some($0 + 1)로 감싸줍니다. 이는 map을 사용해야할 상황에서도 flatMap이 동작하게 만들어서 코드의 의도를 모호하게 합니다.
코드의 모호함은 예기치 않은 동작으로 이어집니다.
struct User {
let name: String?
}
let users = [User(name: "Blob"), User(name: "Math")]
users.flatMap { $0.name } // ["Blob", "Math"]
// 타입 변경 후
struct User {
let name: String // Optional 제거
}
users.flatMap { $0.name } // ["B", "l", "o", "b", "M", "a", "t", "h"]
User 구조체의 name 타입에서 옵셔널을 제거했을 뿐인데 전혀 다른 결과가 나왔습니다.
compactMap 등장
이에 Swift 4.1에서 문제가 되었던 세 번째 flatMap을 deprecated하고 compactMap 메서드를 새로 제공했습니다.
extension Array {
func compactMap<B>(_ transform: (Element) -> B?) -> [B] {
var result = [B]()
for x in self {
switch transform(x) {
case let .some(x): result.append(x)
case .none: continue
}
}
return result
}
}
filterMap이라는 네이밍도 후보에 있었는데요. Ruby의 compact 메서드에 영감을 받아서 compactMap이 되었다고 하네요 ㅎㅎ
compactMap이 제공되면서 배열을 압축한다는 의미가 더 명확해졌고, flatMap의 명확성도 높아졌습니다.
때로는 작은 네이밍 변경이 큰 변화를 가져올 수 있습니다. flatMap에서 compactMap으로 네이밍이 변경되어 Swift의 타입 시스템을 더 일관되고 예측 가능하게 만들었습니다. 우리에게 좋은 네이밍이 얼마나 중요한지를 다시 한 번 일깨워주는 좋은 사례인 거 같네요.
감사합니다.
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.