직접 만들어 보는 map 메서드
직접 만들어 보는 map 메서드
이제 직접 고차 함수(HOF)를 만드는 방법을 봤으니, 좀 더 실무에 가까운 예시를 하나 살펴볼게요. 잠시 List 클래스에 map 메서드가 없다고 상상해 봐요. 여러분이 직접 map을 작성해야 한다면 어떻게 할까요?
함수를 만들 때 좋은 첫걸음은 문제를 정확히 진술하는 것이에요. 우선 List[Int]에만 집중해서 문제를 이렇게 적어봐요.
주어진
List[Int]의 각 요소에 함수를 적용해서, 변환된 요소들을 새로운 리스트로 돌려주는map메서드를 작성하고 싶다.
이 진술을 바탕으로 메서드 시그니처를 쓰기 시작해요. 먼저 함수를 파라미터로 받고 싶고, 그 함수는 Int를 어떤 타입 A로 변환해야 하니까 이렇게 써요.
Scala 2 and 3
def map(f: (Int) => A)
타입 파라미터를 쓰려면 파라미터 목록 앞에 대괄호 [] 안에 선언해야 해요. 그래서 이렇게 추가해요.
Scala 2 and 3
def map[A](f: (Int) => A)
다음으로 map은 List[Int]도 받아야 하니까 이렇게 더해요.
Scala 2 and 3
def map[A](f: (Int) => A, xs: List[Int])
마지막으로 map은 타입 A의 요소를 담은 변환된 List를 반환해야 하므로, 반환 타입도 적어줘요.
Scala 2 and 3
def map[A](f: (Int) => A, xs: List[Int]): List[A] = ???
이렇게 해서 메서드 시그니처가 갖춰졌어요. 이제 남은 건 메서드 본문을 작성하는 일이에요.
map 메서드는 주어진 함수를 주어진 리스트의 모든 요소에 적용해서, 새롭게 변환된 리스트를 만들어요. 이를 구현하는 방법 중 하나가 for 표현식을 쓰는 거예요.
Scala 2
for (x <- xs) yield f(x)
Scala 3
for x <- xs yield f(x)
for 표현식은 코드를 놀랍도록 단순하게 만들어 주는데, 우리 목적에는 이것이 메서드 본문 전체가 돼요. 이제 시그니처와 합치면 List[Int]에서 동작하는 독립적인 map 메서드가 완성돼요.
Scala 2
def map[A](f: (Int) => A, xs: List[Int]): List[A] =
for (x <- xs) yield f(x)
Scala 3
def map[A](f: (Int) => A, xs: List[Int]): List[A] =
for x <- xs yield f(x)
제네릭으로 만들기
보너스로, for 표현식은 List 안의 타입이 Int라는 사실에 의존하는 게 전혀 없다는 점을 눈여겨봐요. 그래서 타입 시그니처의 Int를 타입 파라미터 B로 바꿔도 돼요.
Scala 2
def map[A, B](f: (B) => A, xs: List[B]): List[A] =
for (x <- xs) yield f(x)
Scala 3
def map[A, B](f: (B) => A, xs: List[B]): List[A] =
for x <- xs yield f(x)
이제 어떤 List에서든 동작하는 map 메서드가 생겼어요. 다음 예시들은 map이 의도대로 동작한다는 걸 보여줘요.
Scala 2 and 3
def double(i : Int): Int = i * 2
map(double, List(1, 2, 3)) // List(2, 4, 6)
def strlen(s: String): Int = s.length
map(strlen, List("a", "bb", "ccc")) // List(1, 2, 3)
이번에는 함수를 입력 파라미터로 받는 메서드를 어떻게 작성하는지 살펴봤어요. 다음으로는 함수를 반환하는 메서드를 볼게요.
출처: Scala 3 Book
본문
map을 직접 구현해 보면서 고차 함수를 설계하는 흐름을 따라가 봤어요. 문제를 정확히 진술하는 것부터 시작해서 시그니처를 하나씩 채우고, 마지막에 for 표현식으로 본문을 완성했죠. 그리고 타입 파라미터를 활용하면 List[Int]뿐 아니라 어떤 List에서도 쓸 수 있는 메서드로 일반화할 수 있었어요.