4. 리스트

월, 2006-07-10 13:13 — 귤
1부터 100까지 더하려면 C에선 다음과 같은 방법을 이용한다. ( 100 * 101 / 2 라는 간단한 공식이 있지만 그건 잠시 접어두자. )
int sum = 0; for(int i = 1; i < = 100; i++) sum += i;
이 구문은 다음과 같이 작동한다. 통제변수 i에 1을 대입한 다음, i가 100보다 작거나 같을 동안 i를 1씩 증가시켜주는 것이다. 함수편에서 보았듯이 Haskell에서는 변수라는 개념이 없기 때문에 이런 반복 구조는 사용할 수 없다. 대신 Haskell은 좀 더 쉽고 직관적인 방법을 사용한다.
sum [1 .. 100] 대괄호([])로 묶인 부분을 리스트(list)라고 부른다. 리스트는 여러 개의 값을 한꺼번에 다룰 수 있도록 만든 자료구조이다. 리스트를 정의하는 법에는 세 가지가 있다. 10이하의 짝수로된 리스트를 한 번 만들어보자. 아래에서 –는 주석을 달 때 쓰는 표현으로 Haskell은 그 뒤의 내용은 계산하지 않는다.
even1 = [2, 4, 6, 8, 10] -- 원소나열법 even2 = [2, 4 .. 10] -- 등차수열(arithmetic sequences) evne3 = [x| x ← [1 .. 10], mod x 2 == 0] -- 조건제시법(list comprehension)
원소나열법은 말그대로 리스트의 구성요소를 하나씩 모두 나열해서 적어주는 방법이다. 만약 원소들이 일정한 차이로 반복되는 수열이라면 등차수열을 사용할 수 있다. 그 차이가 1일 때는 첫 값과 마지막 값만 쓰면 되고, 2 이상일 때는 첫 값, 둘째 값, 마지막 값을 쓴다. [1 .. 100]은 1에서 100까지 모든 정수이고 [1, 3 .. 100]은 1에서 100까지 모든 홀수가 된다(100은 짝수이므로 자동으로 빠진다). 조건제시법은 고등학교 1학년 수학에서 배우는 것과 동일한 표기법을 사용한다. 위의 표현은 1에서 10까지 모든 정수 중(x ← [1 .. 10]: ←는 컴퓨터에 <-로 입력한다.)에 2로 나눈 나머지(mod x 2)가 0인(== 0)값이 리스트의 원소(x|)라는 것이다.
이번엔 리스트 안에 있는 값을 모두 더하는 함수를 만들어보자.
sum [] = 0 sum (x:xs) = x + sum xs
:은 원소 하나와 리스트를 결합시켜주는 함수다. [1, 2, 3]은 Haskell에서 내부적으로 1:(2:(3:[]))이라는 식으로 처리된다. 반대로 1:[2, 3]은 [1, 2, 3]과 같다. 따라서 (x:xs)는 리스트에서 맨 앞의 원소를 하나 떼어내 x라 하고 나머지를 xs라고 하자는 표현이다. sum은 함수편에서 계승을 정의할 때 사용했던 순환 구조를 사용하고 있다. Haskell에는 반복 대신 순환이 광범위하게 사용된다. sum [1,2,3]은 아래와 같은 과정을 거쳐 처리된다.
sum [1,2,3] ⇒ sum (1:[2,3]) ⇒ 1 + sum (2:[3]) ⇒ 1 + (2 + sum (3:[])) ⇒ 1 + (2 + (3 + sum [])) ⇒ 1 + (2 + (3 + 0)) ⇒ 6
리스트 안에 있는 값을 모두 더할 뿐만 아니라 곱하고 싶을 수도 있다. 그런데 다 더하든 곱하든 함수의 구조는 똑같다. 그러면 리스트 안에 있는 값들에 어떤 함수를 반복해서 적용하는 그런 함수를 만들어보자.
foldl f x [] = x foldl f x (y:ys) = foldl f (f x y) ys add x y = x + y sum = foldl add 0
foldl 함수는 함수 f에 x와 리스트의 첫 원소 y를 적용한다. 그리고 그 결과와 리스트의 나머지 부분 ys에 다시 foldl을 적용한다. 다시 정의한 sum함수가 어떻게 작동하는 지 보자.
sum [1,2,3] ⇒ foldl add 0 [1,2,3] ⇒ foldl add 0 (1:[2,3]) ⇒ foldl add (0 + 1) (2:[3]) ⇒ foldl (0 + 1 + 2) (3:[]) -- 이하 생략
원래의 sum과 동일한 구조라는 것을 쉽게 알 수 있다. 리스트의 원소 각각에 함수를 적용하는 경우도 있을 수 있다. 이때는 다음과 갈은 코드를 사용한다.
map f [] = [] map f (x:xs) = f x : map f xs
만약 [1, 2, 3]의 모든 원소에 1씩 더하고 싶다면 다음과 같이 한다.
map (1 +) [1, 2, 3] ⇒ 1 + 1 : map (1 +) (2:[3]) ⇒ 1 + 1 : 1 + 2 : map (1 +) (3:[]) ⇒ 1 + 1 : 1 + 2 : 1 + 3 : map (1 +) [] ⇒ 1 + 1 : 1 + 2 : 1 + 3 : [] ⇒ [2,3,4]
지금까지는 리스트에 들어있는 값에 똑같은 함수를 적용하는 것을 해봤는데 반대로 리스트에 들어있는 함수를 똑같은 값에 적용하는 것도 못할 바 없다.
bind [] x = [] bind (f:fs) x = f x : bind fs x
9에 더하기 1, 제곱, 나누기 3을 하고 싶다면 다음과 같이 하면 된다. 전개 과정은 map과 똑같다.
bind [(1 +), (^ 2), (/ 3)] 9
foldl, map, sum 등은 모두 Haskell 표준 함수에 들어가 있기 때문에 굳이 정의해주지 않아도 쓸 수 있다. 대부분 언어에서 표준 함수는 직접 만들기는 커녕 구현을 보고 이해하기도 힘들지만, Haskell에선 입문서에 예제로 쓸만큼 간단하다.