1. 왜 하스켈인가?

 

왜 Haskell 인가?

일, 2006-07-09 20:35 — 귤
함수형 프로그래밍(functional programming:이하 FP)이라는 말이 사람들의 입에 오르내리고 있다. 이미 C++에는 STL이라는 FP 스타일의 표준이 도입되었으며, 널리 사용되는 Python도 FP 문법을 지원하고 있다. FP 프로그래밍이 이렇게 주목받는 이유는 과거의 명령형 프로그래밍에 비해 프로그램을 더 쉽고 간단하게 만들 수 있을 뿐만 아니라 더 안전하고 튼튼하게 만들어주기 때문이다.
그러나 FP는 과거에 한 번씩 유행을 탔던 여느 패러다임과 달리 설명하기가 쉽지 않다. 대부분의 프로그래밍 패러다임들이 새로운 개념을 더하는 방식이었던 반면에, FP는 컴퓨터가 만들어지기도 전에 고안된 순수한 수학적 개념으로 돌아가는 것이기 때문이다. 기존의 프로그래밍 개념에 익숙한 사람들에게는 아무리 말로 설명을 해줘도 이해하기 어렵기 때문에 차라리 함수형 언어를 배우는 편이 더 빠를지도 모른다.
세상에는 여러 가지 함수형 언어들이 있다. 컴퓨터의 역사만큼이나 오래된 LISP도 있고, C만큼이나 빠르다는 소문이 있는 O’Caml도 있다. 그러나 FP를 배우는 데는 하스켈(Haskell)이 가장 낫다. Haskell은 명령형 언어의 군더더기가 붙어있지 않은 순수 함수형 언어기 때문에 불필요한 문법에 시달리지 않고 FP의 개념을 빠르고 쉽게 익힉 수 있다. 실제로 Haskell은 여러 대학에서 프로그래밍에 처음 입문하는 학생들을 가르치기 위한 용도로 널리 쓰이고 있다.
Haskell이 교육용으로만 쓸모 있는 것은 아니다. 하지만 당장 한국에서 C/C++, Java, VB, C#을 제외하면 ‘현장’에서 사용될 가능성은 별로 없기 때문에 Haskell의 실용적 측면에 대해 강조해서 말하는 건 별로 의미가 없다. 그러나 최근 C++, Java, C#에서 추가되고 있는 새로운 FP적 요소라는 게 심하게 말하면 Haskell의 문법이나 기능을 이식한 것에 지나지 않기 때문에 Haskell을 공부하는 것만으로도 주류 프로그래밍 언어를 배우고 사용하기가 훨씬 쉬워진다.
맛보기로 Haskell로 퀵정렬을 살펴보자. 이 알고리즘은 매우 간단하지만 C같은 명령형 언어로는 짜기도 쉽지않고 코드의 길이도 무척 길다. 그러나 하스켈로는 단 두 줄이면 충분하다.
qsort [] = [] qsort (x:xs) = qsort [y| y<-xs, y<x] ++ [x] ++ qsort [y| y<-xs, y>= x]
이것만 읽을 수 있어도 하스켈의 1/3은 배웠다고 할 수 있을 정도로 하스켈의 문법은 단순하다. 여기에서 간단히 설명을 해보도록 하자.
함수형 언어에서는 모든 것이 함수로 취급된다. 바꿔말하면 변수가 없다. 예를 들어 C언어에서 x = 1이라고 하면 x라는 변수에 1을 대입하는 것이다. 이를 배정문(assessment statement)이라고 한다. 하스켈는 변수가 없기 때문에 배정문도 없다. x = 1이라는 표현이 있지만 이는 항상 1을 출력하는 함수 x를 정의한다. 똑같은 말처럼 보이지만 여기에는 큰 차이가 있다. C에서 x가 2도 되고 3도 될 수 있는 반면, 하스켈에서 x는 프로그램이 끝날 때까지 1이다. 이것은 함수형 언어의 다른 특징으로 이어진다. 이들 언어에서 함수에는 부작용 또는 부수 효과(side effect)가 없다. 변수가 없기 때문에 함수는 출력값을 내는 것 외에는 시스템의 어떤 상태도 바꿀 수 없다. 따라서 하스켈의 함수는 수학의 함수와 완전히 동일한 의미를 지닌다. 수학에서 y = f(x) 꼴로 쓰는 함수는 x를 받아 y를 돌려준다. 그 외에는 아무 것도 하지 않으며 x가 바뀌는 일도 없다.
하스켈에서 함수는 개념뿐만 아니라 정의하거나 사용하는 법도 수학과 거의 동일하다. 수학에서 함수를 정의할 때 흔히 f(x) = x + 1와 같이 식을 쓰는데 마찬가지로 하스켈에서도 “함수 입력값 = 출력값”의 형태로 쓴다. 위에서 예로든 퀵 정렬에서 qsort [] = []는 따라서 []를 받아 []를 돌려주는 qsort라는 이름의 함수를 정의한 것이다.
[]는 하스켈에서 리스트(list)를 표현하는데, 이것은 수학에서 집합과 매우 유사한 방식으로 정의하고 사용한다. 리스트는 여러 값을 순서지어 표현하는 방법인데 예를 들어 세 수 1,2,3은 하스켈로 [1,2,3]이라고 쓴다. 리스트는 무한히 많은 값도 표현할 수 있는데 예를 들어 모든 짝수의 리스트는 [2, 4, .. ]이라고 쓰면 된다. 명령형 프로그래밍에서는 무한 루프라고 하여 특수한 경우가 아니면 이를 금기시한다. 왜냐하면 명령형 프로그래밍은 모든 행동을 시간 순서대로 하게 되어 있기 때문에, 어떤 행동이 무한히 길면 프로그램은 거기에서 중단되기 때문이다. 그러나 함수형 프로그래밍에서는 함수들이 논리적 관계만을 맺고 있기 때문에 어떤 값이 무한이 길어도 아무 상관이 없다. 그 값을 사용할 때도 필요한만큼만 계산하면 된다. 이를 소극적 평가법(lazy evaluation)이라고 한다.
집합을 표현하는 방법에는 원소나열법과 조건제시법이 있는 데, 위에서 설명한 것이 원소나열법에 해당한다면 조건제시법에 해당하는 방법도 있다. 수학에서 {x|x < 10, x ∈ A}이라고 쓰면 A라는 집합에 속하는(∈) 원소 중 10보다 작은 것들이라는 뜻이다. 하스켈에선 ∈를 <-라고 쓰는 것만 빼면 똑같은 표현을 사용한다.
리스트에 원소를 더하거나 빼는 것도 간단하다. :은 리스트의 맨 앞에 새로운 원소를 추가하는 기호다. 따라서 1 : [2, 3]이라고 쓰면 [2, 3] 앞에 1을 덧붙여 [1, 2, 3]이 된다. 반대로 맨앞에 있는 원소를 빼내려면 x:xs = [1,2,3]이라고 쓰면 된다. x를 xs에 덧붙여 [1, 2, 3]이 되므로 x는 1이고 xs는 [2, 3]이 된다. 만약 두 리스트를 이어붙이고 싶으면 ++을 사용하면 된다. [1, 2] ++ [3, 4]라고 하면 [1, 2, 3, 4]가 된다.
자, 이것만으로도 하스켈의 1/3은 배웠다고 할 수 있다. 이제 위의 퀵 정렬 코드를 다시 읽어보자. 퀵 정렬은 리스트에 들어있는 원소들을 순서대로 정렬하는 알고리즘이다. 예컨데 [4, 6, 2, 7, 3, 1, 5]을 [1, 2, 3, 4, 5, 6, 7]로 바꾸는 것이다. 만약 빈 리스트라면 정렬할 것이 아무 것도 없다. 따라서 퀵 정렬 함수 qsort [] = []가 된다.
만약 무언가 들어있는 리스트를 qsort에 넣어주면 (x:xs)라는 표현에 의해 맨 앞에 있는 원소를 하나 뺀다. 우리의 [4, 6, 2, 7, 3, 1, 5]을 넣는다면 x는 4, xs는 [6, 2, 7, 3, 1, 5]이다. 그러면 에 의해 4를 맨 가운데, 4보다 작은 수들의 리스트를 왼쪽에([y| y <- xs, y < x]), 4보다 크거가 같은 수들의 리스트([y| y <- xs, y >= x])를 오른쪽에 놓고 ++로 이어붙인다. 계속 같은 예를 이어나가면 [2, 3, 1] ++ [4] ++ [6, 7, 5]이 되겠다. 그러나 4보다 작은 수들이나 큰 수들이나 모두 정렬이 되어있지 않으므로 이들도 똑같이 정렬을 해줘야 한다. 그래서 qsort [y| y < - xs, y < x] ++ ++ qsort [y| y <- xs, y >= x]라는 표현이 되는 것이다. 매우 간단하다.
여기까지 살펴본데서 알 수 있듯이 하스켈은 수학적 표현 거의 그대로 프로그래밍을 할 수 있게 만들어준다. 따라서 프로그래머는 컴퓨터의 구체적인 작동 순서가 아니라, 그 프로그램을 통해 실현하고자 하는 알고리즘에만 집중하면 된다. 또한 모든 함수의 행동은 출력값으로만 나타나기 때문에 프로그래머는 함수의 동작을 완전히 통제할 수 있다. 그래서 segment fault나 memory dump를 일으켜 프로그램이 뻗어버리는 일도 없고, 디버거로 변수의 변화를 눈에 핏발 새워가며 추적할 필요도 없다.