gimmesilver's blog

Agbird.egloos.com

포토로그



C 프로그래머를 위한 하스켈 - 먼저 알아두어야 할 것들 하스켈 스프링노트

먼저 알아두어야 할 것들

 C, C++, JAVA 등의 언어들과 달리 하스켈에는 할당 연산자(assignment operator)가 없습니다. 하스켈에서도 = 연산자가 사용되기는 하지만 이것은 기존의 언어들과 다른 의미를 갖습니다. 예를 들어 C 에서

  1. x = 1;

라고 하면 이것은 '변수 x에 1을 할당한다.' 라는 의미입니다. 하지만 하스켈에서 = 연산자는 'assign to' 가 아니라 'define'을 뜻합니다. 따라서 'x 는 1이다.'라는 뜻을 갖습니다. 한편 하스켈은 순수 함수형 언어입니다. 이 말이 무슨 뜻인가 하면 간단히 말해서 하스켈에서 모든 것들은 다 함수라는 뜻입니다. 예를 들어 순수 객체 지향 언어인 스몰토크에서는 정수나 문자열 등이 모두 객체입니다. 비슷하게 하스켈에서는 정수 1, 2, 3... 등이 해당 값을 반환하는 함수로 취급됩니다(이런 고정 값을 반환하는 함수를 상수 함수라고 합니다). 따라서 하스켈에서 x = 1 라고 한 것은 C 코드로 표현하면 다음과 같습니다.

  1. int x() { return 1; }

 다른 예를 더 들어보자면,

  1. x = x+1;

이라는 코드는 C 언어에서 사용하면 'x 라는 변수에 1을 더해서 다시 x 에 저장해라.' 라는 의미를 갖지만 하스켈에서는 다음과 같이 해석됩니다.

  1. int x() { return x() + 1; }

 즉, x 함수는 무한히 자신을 재귀적으로 호출하는 함수입니다. 따라서 저렇게 x를 정의하고 x 함수를 호출하면 무한 반복 호출을 하며 종료하지 않습니다. 여기서 알 수 있듯이 하스켈에서는 명령형 프로그래밍 언어에서 당연하게 생각하는 '변수에 값을 할당한다'는 개념이 없습니다. 이 때문에 하스켈에서는 함수에 부수 효과(side effect)가 없습니다. 부수 효과(side effect)란 어떤 함수를 실행할 때 그 결과값 외에 다른 어떤 상태가 변화하는 것을 말합니다. 예를 들면 다음과 같은 경우가 있겠습니다.

  1. extern int globalVar;
  2. int foo(int x) {
  3.     ++globalVar;
  4.     return x+globalVar;
  5. }

 위에 있는 foo() 함수는 x+globalVar 식을 계산해서 결과값을 반환하는 작업 외에도 globalVar의 값을 하나씩 증가시키는 부수 효과가 있습니다. 즉, globalVar 라는 변수의 상태가 변화합니다. 따라서 foo()함수는 동일한 입력값 x에 대해서도 globalVar라는 전역 변수의 값이 무엇이냐에 따라 다른 결과값을 출력할 수 있습니다. 하지만 하스켈에서 함수는 부수 효과가 없기 때문에 입력값이 같으면 결과값도 항상 같습니다(이것을 참조 투명성 - referential transparency - 이라고 합니다).

 어쨌든 이런 특성 때문에 하스켈에서는 절차적인 작업을 수행할 때 사용하는 방식이 C 언어와 많이 다릅니다. 예를 들어 반복 작업을 수행할 때 C에서는 while 이나 for 와 같은 loop 문을 사용하지만 하스켈에서는 재귀 호출을 사용합니다. 가령 펙토리얼 함수를 C로 만들면 아래와 같습니다.

  1. int factorial(int x) {
  2.     int ret = 1;
  3.     while (x-- > 0)
  4.         ret *= x;
  5.     return ret;
  6. }

 하지만 하스켈에서는 다음과 같습니다.

  1. factorial x = if x == 0
  2.     then 1
  3.     else x * factorial (x-1)


 C에서는 순차적인 작업을 표현할 때 다음과 같이 코딩을 하지만,

  1. int main() {
  2.     int x, y, z;
  3.     x = foo();
  4.     y = bar(x);
  5.     z = zoo(y);
  6.     return z;
  7. }

 하스켈에서는 함수를 다른 함수의 인자로 직접 전달합니다.

  1. main = zoo (bar (foo))

 하스켈은 코딩 스타일에 대한 몇 가지 문법적인 제약을 가지고 있습니다. 먼저 함수는 항상 소문자로 시작해야 합니다. 반면 클래스나 타입은 반드시 대문자로 시작해야 합니다. 함수 이름에는 알파벳과 숫자를 사용할 수 있으며 특수 문자에서는 작은 따옴표(')와 밑줄(_)을 사용할 수 있습니다. 특수 문자로만으로 이루어진 함수도 정의할 수 있는데 이렇게 특수 문자로 이루어진 함수는 특별히 '연산자(operator)'라고 부릅니다. 일반 함수와 연산자 함수의 가장 큰 차이점은 일반 함수가 전위(prefix) 연산을 수행하는 반면 연산자 함수는 중위(infix) 연산을 수행한다는 것입니다. 또한 C에서는 {} 기호를 사용해서 블럭을 구분하지만 하스켈에서는 소스의 레이아웃을 사용해서 블럭을 구분합니다. 다시 말하면 같은 열에서 시작하는 문장은 같은 레벨의 블럭을 갖습니다. 예를 들어 아래의 C 소스는,

  1. int main() {
  2.     statement1;
  3.     if (...) {
  4.         statement2;
  5.         if (...) {
  6.             statement3;
  7.         } else {
  8.             statement4;
  9.         }
  10.     } else {
  11.         statement5;
  12.     }
  13. }

  하스켈로 표현하면 다음과 같습니다.

  1. main = do
  2.     statement1
  3.     if (...) then do statement2
  4.         if (...) then statement3
  5.             else statement4
  6.         else statement5

 이 때 주의할 점은 하스켈에서는 탭 문자와 공백 문자를 다르게 취급한다는 점입니다. 따라서 가급적 에디터에서 탭 문자를 자동으로 공백문자로 변환하도록 설정하는 것이 좋습니다. 이렇게 하스켈에서는 코딩 스타일에 대한 문법적 제약이 있기 때문에 처음에는 다소 불편하고 어색할 수 있지만 반면 어떤 하스켈 소스라도 스타일 면에서 일관성을 갖게 되는 장점이 있습니다.

 지금까지의 설명을 보고나면 아마 하스켈이 너무 낯설고 어색하다고 느낄지 모릅니다. 특히 변수에 값을 저장할 수 없다는 제약이 매우 크게 느껴질 것입니다. 하지만 실상 하스켈에는 위에서 설명하지 않은 매우 다양한 문법적 특성과 편의성을 가지고 있기 때문에 C나 기타 여러 명령형 언어들보다 간결하면서도 강력한 프로그래밍이 가능합니다. 실제로 하스켈에서 불가능한 프로그래밍은 거의 없으며 성능면에서도 여타 스크립트 언어들에 비해 월등한 결과를 보여줍니다. 그럼 이제부터 본격적으로 하스켈 프로그래밍에 대해서 하나씩 설명해 나가도록 하겠습니다.


덧글

댓글 입력 영역