[Swift] map 파헤치기

Collection에서 map 이 실제로 어떻게 구현되어 있을까요?

naljin
10 min readMay 23, 2021

칭긔칭긔가 자꾸 모나드에 꽂혀서 발표를 한다길래 저도 같이 공부 손민수하다가 “엥.. 나도 map 함수 만들 수 있겠는데?ㅎㅎ” 하는 근거없는.. 자신감이 들고 만것…

하지만 섣불리 도전하기 전에 Swift에서는 어떻게 구현되어있는지 먼저 파보고 싶어져서 ㅎㅎ? 오늘도 글 찌는 노인.. 시작합니다

map(_:)을 찾아서..

[1, 2].map { number in number.description }

이렇게 map 을 사용하고 있다고 해보자구여. 그리고 cmd + click 해서 찾아가보면?! Array 안에 정의되어 있네염

@frozen public struct Array<Element> {
@inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
}

그런데 이 map 함수는 Array 자체에서 제공하는 함수가 아니고, Collection protocol에서 구현되어있는 것이랍니당. (Array, Set, Dictionary 등은 Swift 에서 제공하는 Collection type)

실제로 콜스택을 볼 때 Collection.map 이 호출되구요?

Quick Help에서 나오는 Summary로 검색해봐도

Collection 이랑 Sequence에서만 나오는 것을 확인할 수 있습니다

이러한 결과들로 인해

😎 : 그럼 Collection에 구현되어있는 map을 살펴보면 되겠군!

생각이 들었습니다. 한번 볼까유?

@inlinable
public func map<T>(
_ transform: (Element) throws -> T
) rethrows -> [T] {
// TODO: swift-3-indexing-model - review the following
let n = self.count
if n == 0 {
return []
}

var result = ContiguousArray<T>()
result.reserveCapacity(n)

var i = self.startIndex

for _ in 0..<n {
result.append(try transform(self[i]))
formIndex(after: &i)
}

_expectEnd(of: self, is: i)
return Array(result)
}

으아아악 😫😫😫😫 좀 복잡한게 많아보이는군요.. 시그니처 부분 먼저 뜯어보져

시그니처

@inlinable
public func map<T>(
_ transform: (Element) throws -> T
) rethrows -> [T]

@inlinable

func 최적화 기법 (ex. @inlinable functions, @frozen enum, @frozen struct)

해당 attribute를 function, method, computed property, subscript, convenience initializer, deinitializer 선언에 적용하여, 구현된 내용을 모듈의 public 인터페이스로 노출한다. inlinable symbol에 대한 호출은 구현된 복사본으로 대체 가능하도록 컴파일러에서 허용된다.

흠.. ㅇㅋㅇㅋ 어쨌든 func 최적화 위한거라네요

<T>

Generic 함수를 위해 placeholder type name 을 꺽쇠로 감싼 형태

transform: (Element) throws -> T

Element 타입을 받아서 T 타입으로 반환하는 함수. throws 를 통해 에러를 던질 수 있는 함수라는 것을 알 수 있음.

여기서 ElementCollection에 정의된 associatedtype 이고,,, accociatedtypProtocol을 위한 Generic 이라고 생각하면 되기 때문에, 어쨌든 ElementT도 모두 Generic 입니다

public protocol Collection : Sequence {
associatedtype Element
}

rethrows

자신의 매개 변수로 전달받은 함수가 오류를 던진다는 것을 나타냄

ㅇㅎ.. 위에서 transform 설명할 때 ‘throws 를 통해 에러를 던질 수 있는 함수라는 것을 알 수 있음’ 이라고 말했는데 이거랑 일맥상통하는 말이군요

-> [T]

[T] 타입을 리턴

정리

위의 정보를 다 합치면 이렇게 정리할 수 있습니다.

parameter: Element(Generic)을 받아서 T (Generic)로 반환하는 함수. 해당 함수는 에러를 throw 할 수 있음

return: [T]

위의 예시를 다시 가져와 볼까요?

[1, 2].map { number in number.description }

요 작업에서는 { number in number.description } 가 map 의 인자인 transform에 들어가는 함수가 됩니다.

transfrom(Element) throws -> T 형식이었죠? 따라서 이곳 Element 의 실제 타입은 Int 가 되고, T 의 실제 타입은 String 이 되겠네요 ( number.description 을 return 하니까요!).

이렇게 transform을 인자로 받은 map 함수는 결국 [T] (위의 예시에서는 [String]) 를 리턴하는데, 이를 위해 어떤 처리가 되어있는지 구현부를 살펴봅시다

구현부

let n = self.count
if n == 0 {
return []
}
var result = ContiguousArray<T>()
result.reserveCapacity(n)
var i = self.startIndexfor _ in 0..<n {
result.append(try transform(self[i]))
formIndex(after: &i)
}
_expectEnd(of: self, is: i)
return Array(result)

if n == 0 { return [] }

일단 개수가 0이면 빈 Array 리턴

ContiguousArray

메모리에 연속적으로 저장되는 Array

reserveCapacity(_:)

지정된 수의 element를 저장할 수 있는 충분한 공간을 점유

result.append(try transform(self[i]))

for 문을 돌면서 실행되는 핵심 로직 (time complexity — O(n))

인자로 받은 transform 함수를 실행해서 나온 값을 result array에 append. 이때 transform 이 받는 인자는 self[i] 이고, i는 index

흠..🤔 여기서 위의 예시를 다시 가져와봅시다

[1, 2].map { number in number.description }

여기서 { number in number.description }map 의 인자 transform에 해당했져?

result.append(try transform(self[i]))

그럼 여기서 transform(self[i]) 는 어떻게 해석할 수 있을까요? 바로 self[i]가 인자로 넘어와서 self[i].description 이 실행되는 형태가 될거예요.

만약 self[i] 의 값이 1이면 return 값은 "1" ( 1.description )이 될테고, 이 값이 나중에 map 함수에서 return 될 result array 에 담깁니다(append)

formIndex(after:)

주어진 index를 다음 index로 변환

Replaces the given index with its successor.

이곳에서 index i 값이 변경됩니다. 이를 통해 self[i]로 다음 element에 적절히 접근할 수 있습니다.

시그니처 부분을 자세히 살펴보면 iinout 파라미터로 정의되어 있는 것을 보아 ‘얘 값 자체가 바뀌겠군..’을 유추할 수 있었습니다.

extension Collection{
public func formIndex(after i: inout Index) {
i = index(after: i)
}
}

_expectEnd

index가 CollectionendIndex 가 아니면 에러 메시지 출력 (ArrayShared.swift 에 정의되어 있음)

@inlinable
internal func _expectEnd<C: Collection>(of s: C, is i: C.Index) {
_debugPrecondition(
i == s.endIndex,
"invalid Collection: count differed in successive traversals")
}

정리

위의 정보를 종합해서 구현부에 주석을 달고 다시 찬찬히 살펴봅시당

let n = self.count//0. 개수 0개면 빈 배열 return
if n == 0 {
return []
}
//1. 필요한 공간만큼 배열 만듦
var result = ContiguousArray<T>()
result.reserveCapacity(n)
//2. index 를 위한 i 변수 선언
var i = self.startIndex
//3. for 문 돌면서
for _ in 0..<n {
//4. self[i], 즉, 각 element를 인자로 넣어 transform 함수 실행.
//그 결과로 나온 값은 result 에 append
result.append(try transform(self[i]))

//5. index 다음으로 옮김
formIndex(after: &i)
}
//6. 현재 index가 마지막으로 가있는지 확인
_expectEnd(of: self, is: i)
//7. 변형된 값들이 append 된 Array 반환
return Array(result)

custom map 만들기

자.. 내부 로직을 다 살펴봤으니 이제 저도 custom map 을 작성할 수 있겠다는 자신감이 생기네여?????? 머 비슷한 로직으로 구현해볼 수 있거 아니겠어요? ㅋㅎ

extension Collection {
func myMap<T>(function: (Element) -> T) -> [T] {
var result: [T] = []
self.forEach { (element) in
result.append(function(element))
}
return result
}
}

ㅎㅎ 별거 없쥬? 이제 나도 맵 만들고 쓸 수 있다~~!!!

["sujin"].myMap { $0 + "naljin" } // ["sujinnaljin"]

이제 저녁이나 먹으러 가야지~!~! 끗!

참고

--

--

No responses yet