INDIES

[Learn You a Haskell For Great Good!] 2. 시작하기(2) 본문

Haskell/LYAH

[Learn You a Haskell For Great Good!] 2. 시작하기(2)

jwvg0425 2015. 1. 17. 20:30

Learn You a Haskell For Great Good!


이 게시글은 http://learnyouahaskell.com/chapters 사이트에 올라와있는 글을 한글로 번역한 것입니다.의역이 굉장히 많으니 주의...


2. 시작하기


리스트(list)의 도입


 실세계에서의 쇼핑 리스트와 유사하게, Haskell에서 리스트(list)는 매우 유용해. 리스트는 가장 많이 사용되는 자료구조이고, 수없이 많은 문제들을 풀고 모델링하는데 있어 굉장히 다양한 방식으로 사용될 수 있지. 리스트는 정말 멋져. 이 섹션에서 우리는 리스트의 기본과 문자열(문자열도 리스트야), 그리고 조건제시형 리스트(list comprehension)에 대해 살펴 볼 거야.

 Haskell에서, 리스트는 동일한 타입의 원소들을 담는(homogenous) 자료구조야. 무슨 뜻이냐면 정수들의 리스트나 문자들의 리스트를 만들 순 있지만 몇 개의 정수와 몇 개의 문자를 가지는 리스트는 만들 수 없다는 거야.


: 우리는 GHCI에서 이름(name)을 정의하기 위해 let 키워드를 사용할 수 있어. GHCI에서 let a = 1 이라고 타이핑 하는 건 스크립트에서 a=1이라고 쓰고 그걸 불러오는 것과 똑같아.


  1. ghci> let lostNumbers = [4,8,15,16,23,42]  
  2. ghci> lostNumbers  
  3. [4,8,15,16,23,42]  

위 코드에서 볼 수 있듯이, 리스트는 대괄호와 콤마(,)로 분리되는 리스트의 값들로 표현돼.만약 [1,2,'a',3,'b','c',4]같은 리스트를 만들려고 한다면, Haskell은 문자(Haskell에서 문자는 곁따옴표 사이의 문자로 표현됨)는 숫자가 아니라고 항의할 거야. 문자에 대해 좀 얘기해보자면, 문자열은 문자의 리스트일 뿐이야. "hello"는 ['h','e','l','l','o']를 편하게 축약해서 쓴 거지. 문자열도 리스트기 때문에 문자열에 대해서도 리스트에 관련된 함수들을 쓸 수 있고, 이게 굉장히 편리해.

 두 개의 리스트를 하나로 합치는 건 흔히 있는 일이지. 이럴 땐 ++연산자를 사용하면 돼.

  1. ghci> [1,2,3,4] ++ [9,10,11,12]  
  2. [1,2,3,4,9,10,11,12]  
  3. ghci> "hello" ++ " " ++ "world"  
  4. "hello world"  
  5. ghci> ['w','o'] ++ ['o','t']  
  6. "woot"  

 길이가 긴 문자열에 대해 ++ 연산자를 반복적으로 사용할 땐 주의해야 돼. 두 개의 문자열을 하나로 합칠 때([1,2,3] ++ [4]처럼 리스트 맨 뒤에 원소가 하나인 리스트를 덧붙일 때에도), 내부적으로 Haskell은 ++ 연산자의 왼쪽 편(left side)를 모조리 훑어 봐. 크기가 작은 리스트들을 다룰 땐 별 문제가 아닌데, 500만 개의 원소를 가진 리스트의 맨 뒤에 원소를 하나 덧 붙인다고 하면 아마 시간이 꽤 걸릴거야. 반면에, 리스트의 맨 앞에 어떤 원소를 집어넣을 땐 : 연산자(cons 연산자라고 불러)를 사용하는 편이 속도가 빨라.

++ 연산자가 두 개의 리스트를 취하는 반면 : 연산자는 어떻게 숫자와 숫자들의 리스트, 혹은 문자와 문자들의 리스트를 취하는 지 주의해서 봐봐. ++ 연산자를 이용할 경우에는 리스트 맨 뒤에 원소를 하나만 추가하려는 경우에도, 대괄호를 이용해 해당 원소를 감싸서 리스트로 만들어줘야만 하지.

 [1,2,3]은 실제로는 1:2:3:[]의 간단한 표현(syntactic sugar)이야. []은 텅 빈 리스트지. 우리가 이 앞에 3을 덧붙이면, 이건 [3]이 돼. 그 앞에 2를 붙이면 [2,3]이 되고, 그렇게 계속 붙여나가는 거지.


 : [], [[]], 그리고 [[],[],[]]는 전부 다른 거야. 첫 번째 건 텅 빈 리스트고, 두 번 째건 텅 빈 리스트를 하나 갖고 있는 리스트고, 세 번째건 세 개의 텅빈 리스트를 갖고 있는 리스트지.


 인덱스를 이용해서 리스트의 원소를 갖고 오고 싶다면 !! 연산자를 써. 인덱스는 0부터 시작해.

  1. ghci> "Steve Buscemi" !! 6  
  2. 'B'  
  3. ghci> [9.4,33.2,96.2,11.2,23.25] !! 1  
  4. 33.2  

 하지만, 만약 원소가 4개 밖에 없는 리스트에서 6번째 원소를 가져오려고 시도한다면 에러가 날 테니 조심해야할 거야!

 리스트는 리스트를 포함할 수 있어. 얼마든지 중첩해서 리스트를 포함하는 리스트를 포함하는 리스트를 포함하는... 같은 것도 만들 수 있어.

  1. ghci> let b = [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]  
  2. ghci> b  
  3. [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]  
  4. ghci> b ++ [[1,1,1,1]]  
  5. [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3],[1,1,1,1]]  
  6. ghci> [6,6,6]:b  
  7. [[6,6,6],[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]  
  8. ghci> b !! 2  
  9. [1,2,2,3,4]   

리스트 안의 리스트는 길이는 서로 다를 수 있지만 타입은 달라선 안 돼. 몇 개의 문자와 몇 개의 숫자를 가진 리스트를 만들 수 없는 것과 마찬가지로, 넌 몇 개의 숫자 리스트와 몇 개의 문자 리스트를 가진 리스트를 만들 수 없어.

 리스트는 자신이 포함하고 있는 원소들이 비교될 수 있다면 서로 비교할 수 있어. 리스트끼리 비교하기 위해 <, <=, >, >= 연산자를 사용할 때는, 사전 순에 따라 비교를 진행해. 첫 번째 원소끼리 비교하고, 둘이 서로 같으면 두 번째 원소를 비교하고, 그것도 같으면 세 번째 원소를 비교하고... 하는 식으로.

  1. ghci> [3,2,1] > [2,1,0]  
  2. True  
  3. ghci> [3,2,1] > [2,10,100]  
  4. True  
  5. ghci> [3,4,2] > [3,4]  
  6. True  
  7. ghci> [3,4,2] > [2,4]  
  8. True  
  9. ghci> [3,4,2] == [3,4,2]  
  10. True  

 이것 말고 리스트로 할 수 있는게 뭐가 더 있을까? 리스트와 관련된 기본적인 함수들을 살펴보자.

 head는 리스트를 인자로 받아서 그 머리(head)를 돌려줘. 리스트의 머리는 기본적으로 그 첫 번째 원소를 말해.

  1. ghci> head [5,4,3,2,1]  
  2. 5   

 tail 은 리스트를 인자로 받아서 그 꼬리(tail)를 돌려줘. 다른 말로, 이건 리스트의 머리를 잘라 내.

  1. ghci> tail [5,4,3,2,1]  
  2. [4,3,2,1]   

 last는 리스트를 인자로 받아서 그 마지막 원소를 돌려줘.

  1. ghci> last [5,4,3,2,1]  
  2. 1   

init은 리스트를 인자로 받아서 마지막 원소를 제외한 나머지 모두를 돌려줘.

  1. ghci> init [5,4,3,2,1]  
  2. [5,4,3,2]   

 리스트를 한 마리의 괴물처럼 본다면, 어떤게 어떤 걸 말하는 지는 아래와 같이 표현할 수 있어.


 만약 텅 빈 리스트의 head를 얻으려고 한다면 무슨 일이 일어날까?

  1. ghci> head []  
  2. *** ExceptionPrelude.head: empty list  

 이런! 뭔가 잘못됐어! 어떤 몬스터도 없다면, 당연히 그 머리도 없겠지. head, tail, last, init를 사용할 때 텅 빈 리스트에 사용하지 않게 주의해야돼. 이건 컴파일 타임에는 잡아낼 수가 없고, 따라서 뜻하지 않게 Haskell에게 텅빈 리스트에서 원소를 꺼내달라고 요구하는 걸 미리 방지하는게 항상 좋은 습관이야.

 length는 리스트를 취해서 그 길이를 돌려줘.

  1. ghci> length [5,4,3,2,1]  
  2. 5  

 null은 리스트가 텅 비었는지 확인해줘. 텅 비었다면 True를, 아니라면 False를 돌려주지. 어떤 리스트 xs 에 대해 xs == [] 라고 쓰는 대신에 이 함수를 써.

  1. ghci> null [1,2,3]  
  2. False  
  3. ghci> null []  
  4. True  

 reverse는 리스트를 거꾸로 뒤집어줘.

  1. ghci> reverse [5,4,3,2,1]  
  2. [1,2,3,4,5]  

 take는 숫자와 리스트를 인자로 받아. take는 리스트의 시작점으로부터 여러 개의 원소를 추출하지. 봐봐.

  1. ghci> take 3 [5,4,3,2,1]  
  2. [5,4,3]  
  3. ghci> take 1 [3,9,3]  
  4. [3]  
  5. ghci> take 5 [1,2]  
  6. [1,2]  
  7. ghci> take 0 [6,6,6]  
  8. []  

 리스트의 크기보다 더 많은 원소를 추출할 경우에는 그냥 원래의 리스트를 돌려줘. 0 개의 원소를 추출할 경우에는 빈 리스트를 돌려주고.

 drop은 비슷하게, 리스트의 시작점으로부터 해당 개수의 원소를 없애버려.

 maximum은 어떤 순서를 가진 개체들의 리스트를 받아서 그 중 가장 큰 원소를 돌려줘.

 minimum은 가장 작은 걸 돌려주고.

  1. ghci> minimum [8,4,2,1,5,6]  
  2. 1  
  3. ghci> maximum [1,9,2,3,4]  
  4. 9   

 sum은 숫자들의 리스트를 받아서 그 합을 돌려줘.

 product는 숫자들의 리스트를 받아서 그 곱을 돌려줘.

  1. ghci> sum [5,2,1,6,3,2,5,7]  
  2. 31  
  3. ghci> product [6,2,1,2]  
  4. 24  
  5. ghci> product [1,2,5,6,7,9,2,0]  
  6. 0   

 elem은 개체와 그 개체들의 리스트를 받아서 해당 개체가 리스트 안에 속하는 원소인지를 알려줘. 가독성때문에 보통 이건 중위(infix) 함수로 쓰여.

  1. ghci> 4 `elem` [3,4,5,6]  
  2. True  
  3. ghci> 10 `elem` [3,4,5,6]  
  4. False  

 이것들이 리스트에 대해 쓸 수 있는 몇몇 기본적인 함수들이야. 우리는 이후에 대 많은 리스트 관련 함수들을 살펴볼거야.


Texas ranges


만약 1부터 20까지의 모든 숫자들로 이루어진 리스트를 만들고 싶다면 어떻게 해야 할까? 물론, 그냥 걔네들을 다 적어버리면 되겠지만 자신의 프로그래밍 언어에 탁월함(excellence)을 요구하는 신사들에겐 그런건 해결책이 될 수 없지. 대신에, 우리는 범위(range)를 쓸 거야. 범위(range)는 열거될 수 있는 요소들의 등차수열(arithmetic sequence)을 만드는 방법이야. 숫자들은 열거될 수 있지(enumerated). 1,2,3,4, ... 문자들도 열거될 수 있어. 알파벳은 a부터 z까지 문자들의 열거지. 이름은 열거될 수 없어. "John" 다음에 뭐가 올까? 알 수 없지.

 1부터 20까지의 모든 자연수를 포함하는 리스트를 만들기 위해선 [1..20]이라고 쓰기만 하면 돼. 이건 [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]이라고 쓰는 거랑 똑같고, 원소들의 긴 열거를 손으로 일일히 다 쓰는게 멍청한 짓이라는 것 빼곤 둘 사이에 차이는 없어.

  1. ghci> [1..20]  
  2. [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]  
  3. ghci> ['a'..'z']  
  4. "abcdefghijklmnopqrstuvwxyz"  
  5. ghci> ['K'..'Z']  
  6. "KLMNOPQRSTUVWXYZ"   

  범위는 스텝(step)을 명시할 수 있기 때문에 멋져. 1부터 20까지의 모든 짝수를 포함한 리스트를 만들려면 어떻게 해야할까? 1부터 20 사이의 3의 배수를 모두 포함하는 리스트를 만들려면?

  1. ghci> [2,4..20]  
  2. [2,4,6,8,10,12,14,16,18,20]  
  3. ghci> [3,6..20]  
  4. [3,6,9,12,15,18]   

 그냥 첫 두 원소를 콤마(,)로 구분해서 적고 그 상한선이 어딘지만 표기하면 되는 문제야. 굉장히 영리하긴 하지만, 스텝을 이용한 범위는 사람들이 생각하는 것 만큼 똑똑하진 않아. 2의 모든 제곱수를 얻기 위해 [1,2,4,8,16 .. 100]이라고 쓰면 안 돼. 왜냐하면 넌 제일 첫 스텝 하나만 명시할 수 있고, 등차가 아닌 몇몇 수열들은 처음 몇 개만 가지고 얻어 내기엔 모호하거든.

 20부터 1까지의 모든 원소들을 가진 리스트를 만들고 싶다고 [20..1]이라고 쓰는 건 안 돼. 이럴 땐 [20,19 .. 1]이라고 써야하지.

 범위에서 부동 소수점을 이용할 땐 조심해야돼! 왜냐하면 얘네들은 완전히 정밀하지 못하거든(정의부터 그렇잖아). 범위에서 부동소수점을 이용하는 건 꽤나 파격적인 결과를 갖고 오지.

  1. ghci> [0.10.3 .. 1]  
  2. [0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]  

 난 그냥 리스트 범위에서는 부동소수점을 쓰지 말라고 충고해주고 싶어.

 범위는 상한선을 지정하지 않음으로써 무한대 크기의 리스트를 만드는 것에도 이용할 수 있어. 나중에 무한 리스트에 대해 좀 더 자세히 살펴볼거야. 지금은, 13의 배수중 제일 처음 24개를 가져오는 방법을 한 번 알아보자. 물론 [13,26 .. 13*24]라고 하면 돼. 하지만 더 좋은 방법이 있어. take 24 [13,26..] 라고 쓰는 거지. Haskell은 게으르고, 무한대 크기의 리스트는 절대 끝나지 않을 것이기 때문에, 무한대 크기의 리스트를 바로 평가하려고 하지 않아. Haskell은 네가 무한대 리스트로 부터 뭘 얻어내길 원하는 지 알아내기 위해 기다려. 여기선 네가 그 중 첫 24개를 원한다는 걸 알아낼 수 있고, 기꺼이 시킨 일을 수행하지.

 아래는 무한대 리스트를 생성하는 함수들 중 일부야.

 cycle은 리스트를 인자로 받아서 그걸 무한히 순회하지. 그 결과를 화면에 띄우면, 이건 네가 멈추기 전까지 무한히 반복될거야.

  1. ghci> take 10 (cycle [1,2,3])  
  2. [1,2,3,1,2,3,1,2,3,1]  
  3. ghci> take 12 (cycle "LOL ")  
  4. "LOL LOL LOL "   

 repeat는 원소를 하나 받아서 그 원소들로만 이루어진 무한대 리스트를 만들어. 원소 하나짜리 리스트로 cycle을 쓰는 거랑 비슷하지.

  1. ghci> take 10 (repeat 5)  
  2. [5,5,5,5,5,5,5,5,5,5

 한 원소로만 이루어진 리스트를 만들고 싶다면 replicate 함수를 쓰는 게 더 간단하긴 해. replicate 3 10 은 [10,10,10]을 돌려줘.


난 조건 제시형 리스트(list comprehension)야!


 수학 수업을 한 번이라도 들은 적이 있다면, 아마 집합의 조건 제시법(set comprehension)을 배워본 적이 있을거야. 보통 일반적인 집합에서 더 명시적인 집합을 만들어낼 때 사용되는 방법이지. 처음 10개의 짝수를 포함하는 집합을 만드는 일반적인 조건제시법은이지. 파이프(|) 이전의 부분은 출력 함수(output function)라고 부르고, x는 변수, N은 입력 집합, 그리고 x <= 10은 술어(predicate)야. 이건 자연수를 2배한 모든 숫자 중에서 술어를 만족하는 애들만 포함하는 집합을 의미하지.

 이걸 Haskell에서 사용하고 싶다면, take 10 [2,4 ..]라고 쓰는 걸로 비슷한 걸 얻을 수 있어. 하지만 우리가 첫 10개의 짝수가 아니라 좀 더 복잡한 함수가 적용된 뭔가를 얻고 싶다면 어떻게 해야 할까? 이럴 때 조건 제시형 리스트를 이용할 수 있어. 조건 제시형 리스트는 조건 제시법하고 굉장히 비슷해. 다시 첫 10개의 짝수를 얻는 방법에 대해 알아보자. 조건 제시형 리스트에서 우리는 [x*2 | x <- [1..10]]을 사용할 수 있어. x는 [1..10]에서 얻을 수 있고, [1..10]의 모든 원소(우리가 x와 묶은 - bound)에 대해 두 배를 곱하겠다는 거야. 여기 실제 실행 결과가 있어.

  1. ghci> [x*2 | x <- [1..10]]  
  2. [2,4,6,8,10,12,14,16,18,20]  

봤듯이, 우리는 원하는 결과를 얻었어. 이제 여기에 조건(혹은 술어)을 더해보자. 술어는 묶음부(binding parts) 뒤에 오고 서로 콤마(,)로 구분해. 첫 10개의 짝수 중에서 값이 12이상인 애들만 구한다고 해보자.

  1. ghci> [x*2 | x <- [1..10], x*2 >= 12]  
  2. [12,14,16,18,20]  

 좋아, 잘 동작해. 만약 50부터 100까지 숫자 중에서 7로 나눈 나머지가 3인 숫자들은 어떤 식으로 구할 수 있을까?

  1. ghci> [ x | x <- [50..100], x `mod` 7 == 3]  
  2. [52,59,66,73,80,87,94

 성공이야! 술어(predicate)를 이용해서 리스트에서 필요없는 원소들을 제거하는 건 필터링(filtering)이라고도 부른다는 걸 기억해둬. 이제 다른 예제를 살펴보자. 10보다 큰 홀수는 "BANG!"으로 바꾸고, 그렇지 않은 홀수는 "BOOM!"으로, 홀수가 아니라면 제거해버리는 조건제시형 리스트(comprehension)를 원한다고 하자. 아주 편리하게, 우리는 이 조건 제시형 리스트를 함수 안에 집어넣어서 쉽게 재활용할 수 있어.

  1. boomBangs xs = if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]   

 조건 제시형 리스트의 마지막 부분은 술어야. odd 함수는 숫자가 홀수면 True를, 아니면 False를 리턴해. 원소들은 술어가 True로 평가됐을 때만 리스트에 포함돼.

  1. ghci> boomBangs [7..13]  
  2. ["BOOM!","BOOM!","BANG!","BANG!"

 여러 개의 술어를 포함할 수도 있어. 만약 10부터 20까지의 숫자 중에서 13,15,19를 제외한 나머지를 얻고 싶다면 이렇게 하면 돼.

  1. ghci> [ x | x <- [10..20], x /= 13, x /= 15, x /= 19]  
  2. [10,11,12,14,16,17,18,20]  

 리스트 안에 여러 개의 술어를 포함할 수 있을 뿐만 아니라(결과 리스트의 원소는 모든 술어를 만족해야돼), 여러 개의 리스트로부터 원소를 가져올 수 있어. 여러 개 리스트로부터 원소를 가져올 때, 조건 제시형 리스트(comprehension)는 주어진 리스트들의 모든 조합을 만들어내고 우리가 제공하는 출력 함수로 걔네들을 합쳐. 길이가 4인 리스트 두개를 이용한 조건 제시형 리스트에서 아무런 필터링을 하지 않는다면 결과 리스트는 길이가 16이 될거야. 만약 [2,5,10], [8,10,11] 두 개의 리스트에서 가능한 모든 조합에 대해 둘의 곱을 얻고 싶다면 아래와 같이 하면 돼.

  1. ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]]  
  2. [16,20,22,40,50,55,80,100,110]   

 예측한대로, 만들어진 리스트의 길이는 9야. 이 중 50보다 큰 숫자만 필요하다면?

  1. ghci> [ x*y | x <- [2,5,10], y <- [8,10,11], x*y > 50]  
  2. [55,80,100,110]

형용사 리스트와 명사 리스트를 합치는 조건 제시형 리스트는 어떨까? 그 정돈 아주 쉽지!

  1. ghci> let nouns = ["hobo","frog","pope"]  
  2. ghci> let adjectives = ["lazy","grouchy","scheming"]  
  3. ghci> [adjective ++ " " ++ noun | adjective <- adjectives, noun <- nouns]  
  4. ["lazy hobo","lazy frog","lazy pope","grouchy hobo","grouchy frog",  
  5. "grouchy pope","scheming hobo","scheming frog","scheming pope"]   

알겠어! 이제 우리만의 length 함수를 만들어 보자! length' 정도로 부를 수 있겠지.

  1. length' xs = sum [1 | _ <- xs]   

_ 기호는 list로부터 얻은 원소가 뭔지 신경쓰지 않겠다는 거야. 그래서 사용하지도 않을 변수 이름을 쓰는 대신, 그냥 _라고 쓸 수 있어. 이 함수는 리스트의 모든 원소를 1로 바꾼 다음 그 합을 돌려줘. 결과값은 리스트의 길이임을 의미하겠지.

 이전에 이야기했듯이 문자열도 리스트기 때문에, 우린 스트링을 다루고 만드는 데에 조건 제시형 리스트를 사용할 수 있어. 여기 문자열을 받아서 대문자를 제외한 모든 문자를 제거하는 함수가 있어.

  1. removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]   

 테스트 결과는 다음과 같아.

  1. ghci> removeNonUppercase "Hahaha! Ahahaha!"  
  2. "HA"  
  3. ghci> removeNonUppercase "IdontLIKEFROGS"  
  4. "ILIKEFROGS"   

 이 술어는 전부 잘 동작해. 이 술어는 새 리스트에 포함될 수 있는 문자는 ['A'..'Z'] 리스트에 속한 문자여야한다는 뜻이야. 리스트를 포함한 리스트에 대한 연산을 하는 경우에 조건 제시형 리스트의 중첩(Nested list comprehension)도 가능해. 숫자의 리스트를 원소로 가지는 리스트에서 홀수인 원소들을 모두 제거해보자.

  1. ghci> let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]]  
  2. ghci> [ [ x | x <- xs, even x ] | xs <- xxs]  
  3. [[2,2,4],[2,4,6,8],[2,4,2,6,2,6]] 

  조건 제시형 리스트를 여러 줄에 걸쳐서 쓰는 것도 가능해. GHCI 안에서가 아니라면, 길이가 긴 조건 제시형 리스트는 여러 줄에 걸쳐 쓰는게 좋고 특시 중첩해서 쓰는 경우에는 더 그래. 


튜플(Tuples)


 어떤 면에서, 튜플은 리스트와 비슷해. 튜플도 여러 개의 값들을 하나의 값 안에 저장하지. 하지만, 여기엔 몇가지 근본적인 차이가 있어. 숫자의 리스트는 숫자의 리스트야. 그게 타입이고 얘들은 원소가 한 개 있는 지 무한 개 있는 지는 신경 안 써. 하지만 튜플은 네가 합치고 싶은 원소의 개수가 정확히 몇 개인지 알고 있을 때 쓰고, 그 타입은 구성 요소의 개수와 각각의 타입에 영향을 받아. 튜플은 소괄호와 콤마(,)로 구분되는 구성 요소(component)들로 표현돼.

 또다른 중요한 차이점은 얘네들은 똑같은 타입으로만 구성될 필요가 없다는 거야. 튜플은 리스트랑은 다르게 구성 요소들의 타입이 여러 종류여도 상관없어.

 2차원 벡터를 Haskell에서 어떻게 표현해야할 지 생각해보자. 한 가지는 리스트를 쓰는 방법이야. 이것도 제대로 동작할거야.2차원 평면에서의 도형을 표현하기 위해 여러 개의 2차원 벡터를 리스트에 집어넣고 싶다면 어떻게 해야할까? 아마 [[1,2],[8,11],[4,5]] 처럼 표현할 수 있을거야. 이 방식의 문제점은 [[1,2],[8,11,5],[4,5]] 처럼 표현할 수도 있고 이 것도 역시 숫자의 리스트의 리스트기 때문에 Haskell은 어떤 문제도 없다고 인식할테지만, 우리가 표현하려고 했던 건 이게 아니었지. 하지만 크기가 2인 튜플(페어pair라고도 부름)은 그 자체로 하나의 타입이고, 그 말인즉슨 여러 개의 페어를 포함하고 있는 리스트가 트리플(triple - 크기가 3인 튜플)을 포함할 수 없다는 거야. 리스트 대신에 튜플을 한 번 써보자. 벡터를 둘러싸는 괄호를 대괄호가 아니라 소괄호로 바꿔봐. [(1,2),(8,11),(4,5)]. [(1,2),(8,11,5),(4,5)]같은 도형을 만들려고 시도하면 어떻게 될까? 음, 이런 에러가 발생해.

  1. Couldn't match expected type `(t, t1)'  
  2. against inferred type `(t2, t3, t4)'  
  3. In the expression: (8115)  
  4. In the expression: [(12), (8115), (45)]  
  5. In the definition of `it': it = [(12), (8115), (45)]  

 이건 우리가 페어와 트리플을 같은 리스트에 집어넣으려고 시도했다고 말해주는 거야. 이외에도 [(1,2),("One",2)] 같은 리스트도 만들 수 없어. 왜냐하면 리스트의 첫번째 원소가 숫자의 페어이고, 두번째 원소가 문자열과 숫자로 이루어진 페어기 때문이야. 튜플은 다양한 종류의 데이터를 표현하는데 사용될 수 있어. 예를 들어, Haskell에서 누군가의 이름과 나이를 표현하고 싶다면, 트리플을 사용하면 돼. ("Cristopher", "Walken", 55). 이 예제에서 볼 수 있듯이 튜플은 리스트(문자열=리스트)도 포함할 수 있어.

 데이터의 일부 조각이 가져야만 하는 구성 요소가 얼마나 많은 지 미리 알 수 있을 때에 튜플을 써. 튜플은 크기가 다르면 서로 다른 타입으로 취급하기 때문에 좀 더 엄격하고, 따라서 튜플의 맨 앞에 원소를 추가하는 일반적인 함수같은 건 만들 수 없어. 대신에 페어의 맨 앞에 원소를 붙이거나, 트리플의 맨 앞에 원소를 붙이거나, 크기가 4인 튜플의 맨 앞에 원소를 붙이거나 하는 식으로 함수를 만들어야해.

  원소가 하나인 리스트는 존재할 수 있지만, 원소가 하나인 튜플 같은 건 없어. 왜 그런지는 조금만 생각해보면 알거야. 원소가 하나인 튜플은 그게 포함하는 원소와 똑같아. 그래서 그런걸 따로 만들 이유가 없지.

 리스트와 비슷하게, 튜플도 그 구성 원소들이 비교가 가능하다면 서로 비교할 수 있어. 다만 리스트는 크기가 서로 달라도 비교가 가능한 반면 튜플은 크기가 다르면 비교가 불가능해. 페어에 대해 사용가능한 유용한 함수 두 가지가 있어.

 fst는 페어를 인자로 받아서 그 첫번째 구성요소를 돌려줘.

  1. ghci> fst (8,11)  
  2. 8  
  3. ghci> fst ("Wow"False)  
  4. "Wow"  

 snd는 페어를 인자로 받아서 그 두번째 구성요소를 돌려줘.


 : 이 함수들은 페어에 대해서만 동작해. 트리플, 4-튜플,5-튜플, 기타등등에 대해선 동작하지 않아. 다른 방식으로 튜플에서 데이터를 추출하는 방법은 좀 이따가 살펴볼거야.


 페어들의 리스트를 만들어내는 아주 멋진 zip이라는 함수가 있어. 이건 두 개의 리스트를 받아서 그 둘의 원소들을 각각 매칭시켜 하나의 리스트로 압축해버려. 이건 정말로 간단한 함수지만 쓸모가 많아. 이건 두 개의 리스트를 합치거나 두 개의 리스트를 동시에 순회할 때 특히 유용해. 여기 그에 대한 증명이 있어.

  1. ghci> zip [1,2,3,4,5] [5,5,5,5,5]  
  2. [(1,5),(2,5),(3,5),(4,5),(5,5)]  
  3. ghci> zip [1 .. 5] ["one""two""three""four""five"]  
  4. [(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]  

 이건 원소들을 짝지어서 하나의 리스트로 만들어주지. 첫번째 원소끼리 짝짓고, 두번째 원소끼리 짝짓고, ... 페어는 서로 다른 타입을 담을 수 있기 때문에 zip 함수 역시 서로 다른 종류의 리스트를 인자로 받아서 하나로 합칠 수 있어. 만약 두 리스트의 길이가 다르다면 어떻게 될까?

  1. ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"]  
  2. [(5,"im"),(3,"a"),(2,"turtle")]  

 간단하게 더 작은 쪽에 맞춰서 길이가 긴쪽의 리스트가 잘려나가. Haskell은 게으르기 때문에, 유한 크기의 리스트와 무한 크기의 리스트를 서로 zip할 수도 있어.

  1. ghci> zip [1..] ["apple""orange""cherry""mango"]  
  2. [(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]  


 튜플과 조건 제시형 리스트를 같이 다루는 문제가 있어. 각 변의 길이가 모두 10이하의 정수이고 그 둘레는 24인 직삼각형엔 어떤게 있을까? 먼저, 모든 변의 길이가 10 이하인 삼각형을 만들어보자.

ghci> let triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ]  

 단순히 세 개의 리스트로부터 원소를 받아서 그걸 하나로 합치기만 하면 돼. 이제 GHCI에서 triangles라고 타이핑하면 모든 변의 길이가 10이하인 삼각형의 가능한 목록을 모두 얻을 수 있어. 이제, 여기에 이 삼각형이 직삼각형이라는 조건을 더해보자. 직삼각형의 각 변은 빗변보다 길 수 없고, 두 변의 제곱의 합은 빗변의 제곱과 같다는 조건을 만족하면 되니까 단순히 이 조건을 더해주기만 하면 돼.

  1. ghci> let rightTriangles = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2]   

 이제 거의 다 됐어.이제, 그 둘레가 24라는 조건만 추가해주면 돼.

  1. ghci> let rightTriangles' = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24]  
  2. ghci> rightTriangles'  
  3. [(6,8,10)]  

 이게 정답이야! 그리고 이게 함수형 프로그래밍을 하는 일반적인 방법이야. 해의 집합을 구하는 것으로부터 시작해서, 거기에 변형을 적용하는 과정을 거듭해서, 올바른 결과를 얻을 때까지 걸러내면 돼.