앞서 리스트 기초 장에서는 리스트 타입의 정의와 head
, tail
, length
등 기본적인 리스트 함수들을 살펴보았습니다. 리스트는 이런 기본적인 함수 외에도 map
, filter
, foldr
처럼 함수를 인자로 받는 함수들을 제공합니다. 하스켈에서는 함수가 다른 함수를 인자로 받거나 함수를 리턴할 수 있는데 이런 특성을 가진 함수들을 고차 함수(higher-order function)라고 부릅니다.
map 함수
우선 대표적인 고차 함수의 예로 map
을 살펴보겠습니다. map
의 타입은(a -> b) -> [a] -> [b]
입니다. 타입 a
를 받아 b
를 리턴하는 함수 하나와 a
타입의 리스트 하나를 인자로 받아 b
타입의 리스트를 리턴하는 함수입니다. map
은 주어진 함수를 리스트 각각의 원소에 적용하여 새로운 리스트를 리턴합니다. 간단한 사용 예는 다음과 같습니다.
> map (\x -> x + 1) [1,2,3]
[2,3,4]
여기서 \x -> x + 1
은 하스켈에서 람다 함수(lambda function)을 정의하는 방법입니다.\
를 사용하는 이유는 아스키 코드 중에서 그리스 알파벳 람다(λ)를 가장 닮았기 때문입니다.
이 람다 함수는 인자 x
를 받아서 x + 1
을 리턴합니다. ->
를 기준으로 왼쪽은 인자, 오른쪽은 함수 몸통(function body)입니다. 인자가 2개 이상인 경우는 공백 문자로 구분해 주면 됩니다. 따라서 인자 x
와 y
를 받아 합을 리턴하는 함수는 \x y -> x + y
로 정의할 수 있습니다.
앞서 하스켈 함수에서 배운 섹션(section)을 사용하면 같은 함수를 좀 더 간결히 표현할 수 있습니다. \x -> x + 1
은 2개의 인자를 받는 (+)
연산자에 인자 하나만 먼저 적용하여 새로운 함수를 만들었다고 볼 수 있기 때문에 (+1)
과 같이 표현할 수 있습니다.
> map (+1) [1,2,3]
[2,3,4]
filter
filter
함수는 각 원소에 대해 Bool
을 리턴하는 테스트 함수(predicate)를 인자로 받아 각 원소 중에서 테스트를 통과한 원소들만 모아 새로운 리스트로 리턴하는 함수입니다.filter
의 타입은 (a -> Bool) -> [a] -> [a]
인데 첫 번째 인자 타입 a -> Bool
을 보면 테스트 함수는 리스트의 원소 타입 a
를 인자로 받아 Bool
을 리턴하는 것을 확인하실 수 있습니다.
> filter odd [1,2,3,4,5]
[1,3,5]
filter
와 비슷한 함수로는 partition
함수가 있는데, filter
와 마찬가지로 테스트 함수를 받아 리스트 각 원소에 대해 테스트를 실시하는데, filter
는 테스트를 통과한 원소의 리스트를 리턴하는 반면 partition
은 테스트를 통과한 원소와 통과하지 못한 원소를 따로 모아 각각을 리스트로 리턴합니다.
리턴값이 2개의 리스트이므로 튜플을 사용합니다. 튜플의 첫 번째 원소는 테스트를 통과한 원소들의 리스트이고, 튜플의 두 번째 원소는 테스트를 통과하지 못한 원소들의 리스트입니다. partition
함수는 filter
와 달리 Prelude
모듈에 정의되어 있지 않으므로 사용하려면 먼저 Data.List
모듈을 임포트해주어야 합니다.
> import Data.List
> partition odd [1,2,3,4,5]
([1,3,5],[2,4])
foldr
마지막으로 살펴볼 함수는 foldr
입니다. 리스트의 경우 foldr
의 타입은(a -> b -> b) -> b -> [a] -> b
입니다. 알서 살펴본 map
이나 filter
와는 달리 리턴 타입이 [a]
가 아니라 b
임을 볼 수 있습니다. map
처럼 리스트 각 원소를 가지고 새로운 리스트를 만들거나, filter
처럼 리스트의 각 원소 중 일부를 추려서 새로운 리스트를 리턴하는 함수는 아님을 짐작할 수 있습니다.
이해를 돕기 위해 먼저 사용 예를 살펴보겠습니다. 다음은 foldr
을 이용해 리스트 원소들의 합을 구하는 코드입니다.
> foldr (+) 0 [1,2,3,4,5]
15
(+)
는 인자 두 개를 받아 합을 리턴하는 함수이고, 0
은 초기값입니다. 위 foldr
함수를 풀어써보면 다음과 같습니다.
(1 + (2 + (3 + (4 + (5 + 0 )))))
리스트 기초에서 살펴본 리스트 [1,2,3,4,5]
의 정의와 비교해보면 모양이 상당히 유사하다는 사실을 알 수 있습니다.
(1 : (2 : (3 : (4 : (5 : [])))))
리스트 정의에서 (:)
가 (+)
로 바뀌었고, []
가 0
으로 바뀌었습니다. 즉, foldr f acc
의 의미는 리스트를 펼쳐놓고 (:)
를 f
로 []
를 acc
로 바꾼 것과 같다고 생각할 수 있습니다.
이번에는 각 리스트의 곱을 구해보겠습니다.
foldr (*) 1 [1,2,3,4,5]
마찬가지로 리스트를 정의에 따라 풀어 쓰고 (:)
를 (*)
로 []
를 1
로 바꿔쓰면 됩니다.
(1 : (2 : (3 : (4 : (5 : [])))))
(1 * (2 * (3 * (4 * (5 * 1 )))))
foldr
을 이용하면 리스트의 길이를 리턴하는 length
함수도 구현할 수 있습니다. 아래 리스트 정의에서 (:)
와 []
를 어떻게 바꾸면 각 원소 대신에 리스트의 길이를 계산하게 될지를 생각해 봅시다.
(1 : (2 : (3 : (4 : (5 : [])))))
금방 답이 떠오르지 않으신가요? 이해를 돕기 위해 이번에는 (:)
연산자를 infix가 아닌 prefix로 표기해보겠습니다.
(:) 1 ((:) 2 ((:) 3 ((:) 4 ((:) 5 []))))
(:)
를 보기 좋게 임의의 함수로 f
로 대체하면 다음과 같습니다.
f 1 (f 2 (f 3 (f 4 (f 5 []))))
이제 함수 f
를 어떻게 정의하면 리스트의 길이를 구할 수 있을까요? 길이를 구할 때 리스트의 원소가 무엇인지는 중요하지 않으므로 첫 번째 인자를 무시합니다. 그리고 f
함수가 매번 호출될 때마다 리스트의 길이가 1만큼 늘어난다는 사실에 착안하여 두 번째 인자로 넘어온 값에 1을 더해주면 어떨까요? 그렇게 f
를 정의하면 \x y -> y + 1
가 되는데, 실제로 테스트해보면 리스트의 길이를 리턴하는 것을 볼 수 있습니다.
> foldr (\x y -> y + 1) 0 [1,2,3,4,5]
5
foldr
함수는 맥가이버 칼로 불릴 만큼 굉장히 강력한 함수입니다. foldr
함수를 이용하면 map
이나 reverse
함수도 구현할 수 있습니다.
숙제: foldr
로 map
과 reverse
를 구현해 봅시다.
Pingback: 하스켈 리스트와 고차함수 – 서광열의 하스켈 스쿨