[Swift] map 파헤치기
칭긔칭긔가 자꾸 모나드에 꽂혀서 발표를 한다길래 저도 같이 공부 손민수하다가 “엥.. 나도 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
를 통해 에러를 던질 수 있는 함수라는 것을 알 수 있음.
여기서 Element
는 Collection
에 정의된 associatedtype
이고,,, accociatedtyp
은 Protocol
을 위한 Generic
이라고 생각하면 되기 때문에, 어쨌든 Element
도 T
도 모두 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에 적절히 접근할 수 있습니다.
시그니처 부분을 자세히 살펴보면 i
가 inout
파라미터로 정의되어 있는 것을 보아 ‘얘 값 자체가 바뀌겠군..’을 유추할 수 있었습니다.
extension Collection{
public func formIndex(after i: inout Index) {
i = index(after: i)
}
}
_expectEnd
index가 Collection
의 endIndex
가 아니면 에러 메시지 출력 (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"]
이제 저녁이나 먹으러 가야지~!~! 끗!
참고
- CollectionTypes
- [번역][WWDC][Xcode] Binary Frameworks in Swift 살짝 정리 — XCFramework
- Attributes
- Swift 5.4: Attributes (특성)
- Generics
- associatedType
- reserveCapacity(_:)
- [Swift] Map 함수 구현해 보기
- formIndex(after:)
- Performance of Map, Filter, Reduce, and flatMap vs. for-in loop in Swift
- Swift’s map and filter functions time complexity