[Swift] Map 함수 구현해 보기
개요
함수형 프로그래밍을 하면서 가장 자주 사용하는 함수라면 Map
, Reduce
, Filter
를 들 수 있을 것 같습니다. 자주 사용은 하는데 실제 내부적으로 어떻게 구현돼 있는지, 직접 구현 할수는 없는지 궁금하지 않으신가요? 😄
이 포스트에서는 Map
, Reduce
, Filter
함수 중 Map
함수를 어떻게 구현할 수 있는지 알아보도록 하겠습니다.
이 포스트를 제대로 이해하기 위해서는 Closure와 Generic에 대한 이해가 선행돼야 합니다. 만약 익숙하지 않으시다면 Swift 공식문서의 Closure(영어, 한국어)와 Generic(영어, 한국어) 섹션의 정독을 권해드립니다.
Map 함수의 동작
map
함수를 실행하면 주어진 입력값을 변형해 반환합니다. 다시말해, map
은 입력값을 변형(transforming) 해주는 함수입니다.
예를 들어, 입력값이 다음과 같다면
let array = [1,2,3,4,5]
이 입력값을 각각 1씩 증가시키거나, 2배로 만드는 기능을 수행하기 위해 다음과 같이 map
함수를 사용할 수 있습니다.
let incresedResult = array.map{ $0 + 1 }
let doubledResult = array.map{ $0 * 2 }
map
함수를 실행한 결과는 각각 다음과 같습니다.
incresedResult : [2, 3, 4, 5, 6]
doubledResult : [2, 4, 6, 8, 10]
그럼 지금부터 map
함수를 어떻게 구현할 수 있는지 위 예제로부터 하나하나 추적해 나가 보겠습니다.
map
함수가 실행한 두 기능을 함수로 구현해 보겠습니다.
Map 함수의 구현
먼저 주어진 배열의 각 원소값을 1씩 증가시켜 반환하는 함수를 구현하면 다음과 같이 구현할 수 있을 것입니다.
func incrementArray(xs : [Int]) -> [Int] {
var result = [Int]()
for x in xs {
result.append(x + 1)
}
return result
}
주어진 배열의 원소를 각각 순회하면서 원소에 1을 더한 값을 다른 배열에 넣어 그 배열을 반환합니다.
이제 배열의 각 원소값을 2배한 후 반환하는 함수를 구현해 보도록 하겠습니다.
func doubleArray(xs: [Int]) -> [Int] {
var result = [Int]()
for item in xs {
result.append(item * 2)
}
return result
}
앞서 구현한 함수와 거의 비슷하고 내용중에 1줄만 다릅니다.
result.append(x + 1)
or
result.append(item * 2)
위 차이를 바탕으로 Int
배열을 변환하는 공통된 함수를 정의해 보면 이렇게 만들 수 있을 것입니다.
func computeIntArray(xs: [Int]) -> [Int] {
var result = [Int]()
for item in xs {
result.append(/* x에 관한 함수 */)
}
return result
}
x
의 변화에 대한 내용을 함수로 입력받고, 실행하도록 구현하면 아래와 같이 구현할 수 있습니다.
func computeIntArray(xs: [Int], f: (Int) -> Int) -> [Int] {
var result = [Int]()
for x in xs {
result.append(f(x))
}
return result
}
computeArray
는 f: (Int) -> Int
라는 Int
를 각각 입력값과 출력값으로 갖는 함수 이고, 이 함수는 result.append()
내에서 f(x)
라는 함수로 실행됩니다.
이 함수를 이용해서 앞에 incrementArray
, doubleArray
함수를 다음과 같이 구현할 수 있습니다.
func incrementArray(xs: [Int]) -> [Int] {
return computeIntArray(xs: xs) { $0 + 1 }
}
func doubleArray2(xs: [Int]) -> [Int] {
return computeIntArray(xs: xs) { $0 * 2 }
}
뭔가 그럴듯해 보입니다. 그러면 거의 끝난 것일까요? 아닙니다. map
에서는 다음의 경우도 처리할 수 있습니다.
func isEvenArray(xs : [Int]) -> [Bool] {
return computeIntArray(xs: xs) { $0 % 2 == 0 }
}
isEvenArray
는 Int배열을 입력으로 받아 각 원소가 짝수인지 아닌지에 대해 판별한 Bool
값을 결과 배열로 반환합니다. 이 계산에 computeIntArray
함수를 사용하면 타입에러가 발생합니다. 왜냐하면 이 함수는 Int -> Int
를 인자로 사용하는데 isEvenArray
함수에서는 Int -> Bool
을 필요로 하기 때문입니다.
그렇기 때문에 isEvenArray
함수에서 computeIntArray
를 사용하기 위해서는 computeIntArray
를 다음과 같이 수정해야 합니다.
func computeIntArray(xs: [Int], f: (Int) -> Bool) -> [Bool] {
var result = [Bool]()
for x in xs {
result.append(f(x))
}
return result
}
처음에 작성한 computeIntArray
과 비교했을 때 일부값의 타입이 Int
에서 Bool
로 변경되었습니다.
반환값의 형이 다를 때마다 computeIntArray
를 재정의 해서 사용해야 한다면 확장성이 없어 비효율적일 것입니다. 이럴 때 사용하라고 Swift에서는 Generic를 제공해줍니다. 😄 Generic을 사용해 computeIntArray
를 다음과 같이 구현할 수 있습니다.
func genericComputeArray<U>(xs: [Int], f: (Int) -> U) -> [U] {
var result = [U]()
for x in xs {
result.append(f(x))
}
return result
}
여기서 한발 더 나아가 [Int]
와 (Int)
도 타입의 제한없이 사용할 수 있도록 generic 파라미터 T
를 추가해보도록 하겠습니다.
func customMap<T, U>(xs: [Int], f: (T) -> U) -> [U] {
var result = [U]()
for x in xs {
result.append(f(x))
}
return result
}
이제 저희가 구현한 customMap
을 이용해 isEvenArray
와 doubleArray3
를 구현하면 다음과 같습니다.
func isEvenArray(xs : [Int]) -> [Bool] {
return customMap(xs: xs) { $0 % 2 == 0 }
}
func doubleArray3(xs: [Int]) -> [Int] {
return customMap(xs: xs) { $0 * 2 }
}
두 함수의 반환 값이 각각 [Bool]
과 [Int]
이지만 customMap
함수를 generic으로 구현했기 때문에 두 함수 다 잘 동작하는 것을 확인할 수 있습니다.
[소스코드]
let array = [1,2,3,4,5]
let isEvenResult = isEvenArray(xs: array)
let doubleResult3 = doubleArray3(xs: array)
print("isEvenResult : \(isEvenResult)")
print("doubleResult3 : \(doubleResult3)")
[실행결과]
isEvenResult : [false, true, false, true, false]
doubleResult3 : [2, 4, 6, 8, 10]
여기서 끝이 아닙니다. map
에 최대한 가까기 위해 마지막으로 한걸음 더 들어가 보겠습니다. map
은 배열을 입력받아서 실행되지 않고 배열에서 직접 이루어 집니다. 이것을 지원하기 위해 Array
클래스에 customMap
을 확장해 추가해 보도록 하겠습니다.
extension Array {
func customMap<U>(f: (Element) -> U) -> [U] {
var result = [U]()
for x in self {
result.append(f(x))
}
return result
}
}
Array
의 인자는 T
대신 Element
라는 지정된 파라미터를 사용합니다. 드디어 다 끝났습니다. 😎
Map 함수와 CustomMap 함수의 동작
이제 Swift에서 기본적으로 제공해주는 map
의 동작과 저희가 구현한 customMap
의 동작을 비교해 보도록 하겠습니다.
[소스코드]
let array = [1,2,3,4,5]
let mapResult1 = array.map{ $0 + 1 }
let customMapResult1 = array.customMap{ $0 + 1 }
let mapResult2 = array.map{ $0 * 2}
let customMapResult2 = array.customMap{ $0 * 2 }
let mapResult3 = array.map{ $0 % 2 == 0 }
let customMapResult3 = array.customMap{ $0 % 2 == 0 }
print("Map #1 : \(mapResult1)")
print("CustomMap #1 : \(customMapResult1)")
print("==============================")
print("Map #2 : \(mapResult2)")
print("CustomMap #2 : \(customMapResult2)")
print("==============================")
print("Map #3 : \(mapResult3)")
print("CustomMap #3 : \(customMapResult3)")
[실행결과]
Map #1 : [2, 3, 4, 5, 6]
CustomMap #1 : [2, 3, 4, 5, 6]
==============================
Map #2 : [2, 4, 6, 8, 10]
CustomMap #2 : [2, 4, 6, 8, 10]
==============================
Map #3 : [false, true, false, true, false]
CustomMap #3 : [false, true, false, true, false]
Swift에서 제공하는 map
함수와 저희가 만든 customMap
함수가 동일하게 동작하는 것을 확인할 수 있습니다.
결론
map
함수는 어떤 마법의 함수가 아니라 우리 모두 직접 만들 수 있는 함수입니다. 모든게 그런 것 같습니다. 알면 쉽고 모르면 어려워 보이는. 😄
이상 map
함수에 대해 알아보고 직접 구현해 보았습니다. 이후 포스트에서는 나머지 함수 reduce
,filter
함수도 직접 구현해 보겠습니다.