[Haskell] 06. 리스트 타입 가지고 놀기 (2) – 리스트 만들기

리스트를 만드는 방법에는 여러가지가 있다. 지금까지 우리가 했던 일반적인 방법은 [1,2,3,4,5]와 같이 직접 입력하는 것이다. 하지만 1~100까지 혹은 1~10000까지의 리스트를 만든다면 직접 손으로 입력하기는 어렵다. Haskell은 생각보다 똑똑하다. 다음 예제를 보자.

Prelude> [1,2,3,4,5]
[1,2,3,4,5]
Prelude> [1..5]
[1,2,3,4,5]
Prelude> [1..14]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14]
Prelude> [1,3..15]
[1,3,5,7,9,11,13,15]
Prelude> [1,4..15]
[1,4,7,10,13]

Prelude> ['a','b'..'f']
"abcdef"
Prelude> ['a'..'z']
"abcdefghijklmnopqrstuvwxyz"
Prelude> ['a','c'..'z']
"acegikmoqsuwy"

[1..5]와 같은 표현을 사용할 수 있다. 또한 1~15 범위의 홀수만을 리스트에 넣고 싶을 때는 [1,3..15]로 표현할 수 있다. 인간이 이해할 수 있는 표현 정도이면 Haskell도 이해할 수 있다. 컴퓨터를 너무 무시하지 말자. 더 놀라운 것은 [1..]와 같은 표현도 사용할 수 있다. 이것은 1부터 무한대까지의 수를 표현하는 것으로 자연수 \(\mathbb{N}\)을 표현한다. Haskell이 아닌 다른 언어로 이런 표현을 구현하는 것은 쉽지 않다.

이제 리스트를 만드는 데 좀 더 수학적인 표현을 알아보자. 또 다시 어렸을 적, 우리는 집합의 요소를 표현하는 방법을 배웠다. 당연히 기억은 안 난다. 하지만 보면 비슷한 게 떠오를 수도 있다. 예를 들어 자연수의 집합을 다음과 같이 표현했다.

\begin{equation}
{ x \ | \ x \in \mathbb{N} }
\end{equation}

이런 표현 기억이 나는가? 다른 집합을 나타내는 표현들도 보자.

\begin{align}
{ x \ | \ x \in \mathbb{N}, \quad x \ \text{mod} \ 2 = 0 } \qquad \text{(1)}\\
{ x \ | \ x \in {1,2,..,10}, \quad x \ \text{mod} \ 2 = 1 } \qquad \text{(2)}\\
{ (x,y) \ | \ x \in{2,3,4,5}, \ y \in {1,2,3} } \qquad \text{(3)}\\
{ x^2 \ | \ x \in{1,..,5} } \qquad \text{(4)}
\end{align}

식 (1)은 짝수 집합을 나타내고, 식(2)는 1~10 범위의 수 중에 홀수의 집합을 나타낸다. 식 (3)은 \(x\)는 2~5, \(y\)는 1~3의 범위를 갖는 수들의 짝들을 나타낸다. 식 (4)는 1~5 범위의 제곱들의 집합이다. 이 표현들을 Haskell에서의 리스트로 나타내보자.

[x|x<-[1..], even x] -- 식(1)
[x|x<-[1..10], odd x] -- 식(2)
[(x,y)|x<-[2..5], y<-[1,2,3]] -- 식(3)
[x^2|x<-[1..5]] -- 식(4)

수학에서의 식과 매우 흡사하다. 수학적 표현을 컴퓨터로 옮기는 데 모두 한 줄 이내로 마무리 된다. 다른 언어를 이용한다면, for문을 이용해서 작성해야 하기 때문에 이보다 복잡해진다. 식(1),(2),(3),(4)의 실행결과는 아래와 같다. (식(1)의 경우 무한개의 수들이 출력되기 때문에 take 함수를 사용하여 앞의 10개 요소만 취했다.)

Prelude> take 10 [x|x<-[1..], even x] -- 식(1)
[2,4,6,8,10,12,14,16,18,20]
Prelude> [x|x<-[1..10], odd x] -- 식(2)
[1,3,5,7,9]
Prelude> [(x,y)|x<-[2..5], y<-[1,2,3]] -- 식(3)
[(2,1),(2,2),(2,3),(3,1),(3,2),(3,3),(4,1),(4,2),(4,3),(5,1),(5,2),(5,3)]
Prelude> [x^2|x<-[1..5]] -- 식(4)
[1,4,9,16,25]

이렇게 리스트를 생성하는 방법을 Haskell에서는 generator라고 부른다. 이제 몇 가지 응용 예제를 살펴보자.


약수 구하기

어떤 수 n의 약수는 n을 약수로 나누었을 때 나누어 떨어지는 수를 의미한다. 예를 들면, 15의 약수는 {1,3,5,15}가 된다. 32의 약수는 {1, 2, 4, 8, 16, 32}이다. 31의 약수는 {1, 31}이다. 먼저 이를 수학 표현식으로 표현해보자.

\begin{equation}
{ x \ | \ x \in {1,..,n}, \ n \ \text{mod} \ x = 0 }
\end{equation}

이와 같이 표현할 수 있을 것이다. 지금까지의 과정을 충분히 따라왔다면, 이를 Haskell 식으로 옮기는 것은 어렵지 않다.

Prelude> let factors n = [x|x<-[1..n], mod n x == 0]

Prelude> factors 5
[1,5]
Prelude> factors 15
[1,3,5,15]
Prelude> factors 32
[1,2,4,8,16,32]
Prelude> factors 31
[1,31]

실행 결과 주어진 수의 약수를 잘 출력하는 것을 볼 수 있다. 비교를 위해 이를 C++ 코드와 비교해보자. C++에 익숙치 않은 사람들을 위해서 전체 코드를 나타내었다.

#include <iostream>
#include <vector>
using namespace std;

bool factors(int n, vector<int>& v) {
    for(int i=1; i<=n; i++) {
        if (n % i == 0) {
            v.push_back(i);
        }
    }
    return true;
}

int main(int argc, char* argv[]) {
    vector<int> v;
    if (factors(stoi(argv[1]), v)) {
        for(int i=0; i<v.size(); i++) {
            cout << v[i] << endl;
        }
    }
    return 0;
} 

입출력 부분을 제외한 factors() 함수 부분만 보더라도 Haskell에서의 factors n = [x|x<-[1..n], mod n x == 0]와 많은 차이를 보인다. 그만큼 함수형 언어는 복잡한 식을 간결하게 표현할 수 있고, 수학적 표현식과의 비교에서 알 수 있듯이 인간의 언어와 가장 가까운 고수준(high-level)의 언어임을 알 수 있다.


소수 구하기

위의 예제에서 본 것처럼 1과 자기 자신 n은 항상 약수가 된다. 이와 같이 1과 자기 자신 n만 약수를 갖는 수를 소수(prime number)라고 부른다. 먼저 주어진 수 n이 소수인지 아닌지 확인하는 함수를 만들자.

Prelude> let prime n = factors n == [1,n]

Prelude> prime 1
False
Prelude> prime 2
True
Prelude> prime 3
True
Prelude> prime 5
True
Prelude> prime 9
False
Prelude> prime 31
True

이전에 작성했던 factors 함수를 이용하여 [1,n]과 비교를 하면 금방 작성할 수 있다. 그러면 이렇게 하나하나 소수를 테스트 하는 방법 말고, 주어진 수 n보다 작은 모든 소수를 출력하는 함수를 만들어 보자. 금방 작성해야 할 함수의 모형이 떠오르지 않는다면, 먼저 수학식을 생각하자.

\begin{equation}
{ x \ | \ x \in {2,..,n}, \ x \ \text{is a } \ prime \ number. }
\end{equation}

\(x\)가 prime number인지 아닌지 판단하는 함수 prime을 만들었으므로, 이 함수를 이용하자.

Prelude> let primes n = [x|x<-[2..n], prime x]

Prelude> primes 20
[2,3,5,7,11,13,17,19]
Prelude> primes 100
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]

결과값이 정말 소수인지 아닌지는 직접 확인하기 바란다. 나중에 함수의 recursion을 배우게 되면 다른 방법으로 소수를 구하는 함수를 작성할 수 있다. 이전에 배웠던 filter 함수를 이용하면,

let primes n = filter prime [2..n]

와 같이 작성할 수도 있다.


문자열 위치 검색하기

“This is Haskell example codes.” 이 문장에서 ‘e’의 위치는 어디에 몇 번 나오는가? 이런 문제를 생각해 보자. 일단 한 단계를 진행하자. Haskell에서 문자열은 결국 [Char]이므로 리스트로 다룰 수 있다. 먼저 각 문자열과 인덱스를 만들자.

let xs = "This is Haskell example codes."
let tt = [(x, i) | x <- xs, i <- [0..n]] where n = length xs - 1

tt를 출력해 보면, 아마도 원하는 결과를 얻을 수 없을 것이다. 뭔가 잘못되었다. where n = length xs - 1 이라는 부분이 들어갔지만, 일단 무시하자. 단지 i의 범위를 문자열의 길이의 범위로 만들기 위해 사용한 것이다. 여기서 우리가 만든 리스트는 xs[0..n]의 모든 조합인 것이다. 우리가 원하는 모습은 [('T',0),('h',1),('i',2),...('s',28),('.',29)] 이런 것이다. 이를 위해서 zip이라는 함수를 이용하자. zip을 사용한 예제를 보면 기능을 쉽게 이해할 수 있을 것이다.

Prelude> zip [1,2,3] [4,5,6]
[(1,4),(2,5),(3,6)]

Prelude> zip "asdf" [0..10]
[('a',0),('s',1),('d',2),('f',3)]

우리가 원하는 형태의 결과를 반환해 준다. 2개의 리스트 길이가 서로 다른 경우에는 짧은 것을 기준으로 하고, 남은 것들은 버린다. 그러면 위의 Haskell code를 약간 수정하자.

Prelude> let xs = "This is Haskell example codes."
Prelude> let tt = [(x, i) | (x,i) <- zip xs [0..n]] where n = length xs - 1
Prelude> tt
[('T',0),('h',1),('i',2),('s',3),(' ',4),('i',5),('s',6),(' ',7),('H',8),('a',9),('s',10),('k',11),('e',12),('l',13),('l',14),(' ',15),('e',16),('x',17),('a',18),('m',19),('p',20),('l',21),('e',22),(' ',23),('c',24),('o',25),('d',26),('e',27),('s',28),('.',29)]

자 이제 원하는 결과를 얻었다. 원하는 위치를 찾기 위해서는 조금 더 작업을 해야 한다. 여기서 ‘e’의 위치를 반환하도록 조금 수정해 보자.

Prelude> let tt = [(x, i) | (x,i) <- zip xs [0..n], x == 'e'] where n = length xs - 1
Prelude> tt
[('e',12),('e',16),('e',22),('e',27)]

x == ‘e’를 추가함으로 ‘e’의 위치를 알 수 있다. 이제 거의 다 되었다. 우리는 ‘e’를 찾는 것이 아니고, 임의로 입력 받은 문자열을 찾을 것이므로, ‘e’ 대신 w라는 변수를 사용하자. 그리고 함수의 형태를 만들면,

Prelude> let positions w xs = [(x, i) | (x,i) <- zip xs [0..n], x == w] where n = length xs - 1

Prelude> positions 'e' xs
[('e',12),('e',16),('e',22),('e',27)]

Prelude> positions 'e' "This is Haskell example codes."
[('e',12),('e',16),('e',22),('e',27)]

찾고 싶은 문자 w와 문자열 xs의 입력을 받아 해당 문자의 위치를 리스트로 반환하는 함수를 만들었다. 여기서 반환된 리스트의 반복된 ‘e’는 굳이 필요없다. 함수를 약간 수정하자.

Prelude> let positions w xs = [i| (x,i) <- zip xs [0..n], x == w] where n = length xs - 1
Prelude> positions 'e' "This is Haskell example codes."
[12,16,22,27]
Prelude> positions 'l' "This is Haskell example codes."
[13,14,21]
Prelude> length (positions 'l' "This is Haskell example codes.")
3

단순히 해당 문자의 개수만 알고 싶다면 반환된 결과에 length 함수를 이용하여 리스트 안의 요소 개수를 출력하면 된다.

Reply