Languages/Erlang2008.09.04 11:00

C를 배울때 변수에 대해서 배우고 난 다음에, 각종 제어문에 대해서 공부했던 기억이 납니다. 제가 어렸을 적에 영어를 처음 공부할 때 쉬운 단어들부터 하나둘씩 배우고 그 다음에 문장을 어떻게 만드는지를 공부했던 것같은데, 프로그래밍 언어를 공부하는 데도 비슷한 구석이 있는 것 같군요.

암튼 오늘은 Erlang에서의 제어문에 대해서 좀 살펴보도록 하겠습니다. C 프로그래머에게 있어 가장 익숙한 제어문이라면 if-else 같은 것이 있겠고, 드물게 쓰이지만 goto 같은 것도 있겠군요. 제어(control)라는 말이 의미하듯, 제어문은 프로그램의 흐름을 조정하기 위해 사용되는 구문입니다.

그런데 Erlang은 함수형 언어라, 이 제어문이라는 것이 C와 같은 절차형 프로그래밍 언어와는 좀 다른 양상으로 나타나는 경향이 있습니다.

그 이야기를 하기 전에, 우선 Erlang 프로그램 파일이 어떻게 구성되는지부터 한번 살펴보도록 하겠습니다. 앞서 살펴보았던 예제들은 전부 Erlang 셸에서 실행되는 예제들이었는데요. 오늘부터는 완전한 프로그램 파일을 두고 예제를 살펴보려고 합시다. 그러니 Erlang 프로그램의 구조에 대해 알아두는 것이 좋겠죠.

다음은 Erlang 프로그램 파일의 가장 기본적인 형태입니다.

-module(foo).
-export([bar/1, bar/2]).

...

-module은 Erlang 모듈을 정의하기 위한 부분입니다. Erlang 프로그램 파일의 이름이 foo.erl 이라면 (Erlang 프로그램 파일은 erl의 확장자를 갖습니다) 모듈의 이름은 foo 라고 짓습니다. 모듈의 이름은 그 안에 담긴 함수들이 어떤 일을 하는지를 잘 나타내도록 지어야 합니다.

-export는 해당 모듈 안에 정의된 함수 중, 모듈 외부에서 호출하여 쓰일 수 있는 함수의 이름과 그 인자의 개수를 명시하기 위해 사용합니다. 여기에 적히지 않은 함수들은 외부에서 호출될 수 없습니다. 함수들의 이름은 ,로 구분하여 Erlang 리스트 형식으로 나열합니다. 간단하죠?

그 아래쪽에는 이제 Erlang 함수들이 오게 됩니다.

Erlang 이라는 프로그래밍 언어가 함수형 언어이다보니, Erlang에서는 프로그램 코드가 전부 함수를 통해 작성됩니다. C에서는 함수의 시그너춰(signature)를 명시할 때 함수의 반환값 타입, 이름(name), 그리고 인자 리스트 순서대로 적었었죠. 하지만 Erlang에서는 함수의 이름과 인자 리스트만 명시하면 됩니다. 반환값은? 어떤 것이라도 반환할 수 있습니다. -_-; (이 점이 가끔 strong-typed 언어에 익숙한 프로그래머들을 당황하게 하긴 합니다.ㅋㅋ)

그럼 우선 간단한 예제를 보도록 하죠.

-module(geeometry).
-export([area/1]).

area({rectangle, Width, Ht}) -> Width * Ht;
area({circle, R}) -> 3.14159 * R * R.

위의 Erlang 모듈 geometry에는 인자를 하나만 받는 area라는 함수가 정의되어 있습니다. 인자를 받아, 그 인자가 나타내는 도형의 면적을 계산해 주는 함수입니다. C라면 이런 코드를 어떻게 작성했을까요? 코드의 효율성이나 가독성같은 것은 싹 다 무시하고, 가장 간단하게 한번 만들어 봅시다.

double area(int type, double arg1, double arg2) {
    if ( type == 1 ) { // rectangle
        return arg1 * arg2;
    }
    else if ( type == 2 ) { // circle
        return 3.14159 * arg1 * arg1;
    }

    return 0.0;
}

어떤 도형이냐를 판단하기 위해 type을 두었고, 면적을 계산하는 데 쓰일 인자들을 받기 위해 arg1과 arg2를 두었습니다. type의 값이 무엇이냐에 따라 도형의 면적을 계산하는 공식은 달리 적용하고 있습니다.

이 코드의 문제점은, 코드가 의미하는 바를 이해하기 힘들다는 것이고 (의도적으로 가독성을 무시했으니 당연한가요? ㅋㅋ) 한 함수 안에 여러 가지 기능들이 뭉개져 있다는 것입니다. 함수가 기능을 표상하는 것이 아니라, 함수 내의 if-else 블럭들이 기능을 표상하게 됩니다. 그 문제를 해결하기 위해 area_rectangle과 area_circle같은 함수들을 계속 만들게 되면 기능 분리는 제대로 될지 모르지만 함수 이름 오버로딩이 지원되지 않는다는 문제때문에 프로그램 코드는 영 보기 싫어지게 되죠.

말이 좀 길었습니다만, Erlang은 이런 문제들을 꽤 우아하게 피해갑니다. 앞선 글에서도 언급한 바 있는, 변수에 대한 대응 규칙 덕분이죠.

Erlang에서는 함수에 인자를 전달할 때, 그 인자에 최적으로 대응되는 대응 규칙을 가지고 있는 함수에 전달합니다. 가령 {rectangle, 3, 4}와 같은 투플을 위의 모듈이 export하고 있는 area 함수에 전달한다고 해 보죠. 이 인자가 대응될 수 있는 대응 규칙은 {rectangle, Width, Ht} 쪽입니다. 그러니 결국 {rectangle, Width, Ht} = {rectangle, 3, 4}의 대응 연산이 실행되면서 Width와 Ht에는 각각 3과 4가 바인딩 되고, 이 값을 사용해 사각형(인자로 전달된)의 면적을 계산하게 됩니다.

한번 실행해 볼까요?

먼저 프로그램 파일이 있는 디렉터리에서 Erlang 셸을 열고, 다음과 같이 합니다.

1> c(geometry).
{ok, geometry}
2> geometry:area({rectangle, 3, 4}).
12

circle에 대해서도 마찬가지로 할 수 있겠군요.

위의 예제가 시사하는 바는 이런 겁니다. Erlang에서는 함수를 기능 단위로 분할하여 작성하는 것으로 기존 프로그래밍 언어의 제어문이 하던 일을 대체할 수 있다는 것이죠.

물론 그렇다고 Erlang에 If 문이 없는 건 아닙니다. Erlang에도 If문은 있죠. 다만 함수를 통해 기존의 제어문이 어떻게 우아한 형태로 바뀔 수 있는지를 보이기 위해 좀 장황하게 설명을 했을 뿐입니다. 아. 그런데 깜빡하게 말씀 안드린 부분이 있군요.

위에서처럼 하나의 함수를 여러 개의 함수들로 쪼개어 작성할 때, 결국 그것이 하나의 함수이기 때문에, 각각의 부분함수들은 ; 로 구분합니다. 제일 마지막에만 . 로 끝맺음을 하게 되죠. 결국 하나의 함수를 여러 개의 절로 분할하여 작성하는 거라고 이해하시면 되겠습니다. ;는 절을 구분하는 구분자이고, . 는 함수의 마지막을 표시하는 구둣점이라고 생각하시면 되겠네요.

그런데 여기까지 설명드리면 아마 이런 의문이 생기실수도 있겠어요.

"패턴 대응 규칙으로 처리할 수 없는 조건은 어떻게 함수 절로 분할할수 있는가?"

가령 이런 겁니다. max(X, Y)를 Erlang 함수로 구현한다고 해 봅시다. 이 경우에는 단순히 패턴 대응 규칙을 잘 작성하는 것 만으로는 하나의 함수를 절 단위로 분할할 수 없습니다. 그러니 이런 경우에는 가드(guard)라는 것을 사용해야 합니다. 다음의 예제 코드를 보시죠. 붉은 색으로 표시된 부분이 가드입니다. 가드는 when 키워드를 사용해 명시합니다.

max(X, Y) when X > Y -> X;
max(X, Y) -> Y.

X가 Y보다 클 때는 X를 반환하고, 그렇지 않을때는 Y를 반환하는 코드입니다. 주어진 인자를 처리할 수 있는 함수 절을 찾을 때 위에서 아래쪽으로 찾기 떄문에, 아래쪽에는 가드를 적어주지 않았습니다.

하나 이상의 가드를 연결할 떄에는 ,를 사용해 연결할 수도 있고, ;를 사용해 연결할 수도 있습니다. ,를 사용하면 가드 식은 논리적으로 보아 AND로 연결된 것이고, ;를 사용하면 OR로 연결된 것입니다. 더 자세한 내용은 Erlang 교과서 ("프로그래밍 얼랭")를 참조하시기 바랍니다. 시시콜콜한 문법적인 디테일까지를 전부 다 설명하는 것은 피하도록 하겠습니다. :-P

각설하고, 지금까지의 예제는 함수의 '절'을 사용해서 C같은 프로그래밍 언어의 제어문이 하는 일을 흉내내는 것이었습니다. 그런데 Erlang에 제어문을 위한 구문이 없느냐  하면 그건 또 아니거든요. Erlang도 if나 case 등을 지원합니다. 물론, 쓰는 방법은 좀 다릅니다. 예를 들어 설명해 보죠. 위의 max(X, Y) 함수를 case 문을 사용해 바꿔 보면 다음과 같습니다.

max(X, Y) ->
    case (X>Y) of
        true -> X;
        false -> Y
    end.

우선 (X>Y)의 값을 계산한 다음에 (C의 switch-case 문과 하는 일이 똑같습니다) 그 값이 대응되는 규칙을 case 문 안의 각각의 절 왼쪽에서 찾아서, 발견되면 그 오른쪽에 오는 식을 계산해 값을 반환하는 형태입니다.

if 문으로 위의 코드를 바꿔 쓰면 다음과 같습니다.

max(X, Y) ->
    if
        (X>Y) -> X;
        true -> Y
    end.

if 문 안에 오는 각각의 절들은 위에서 아래로 검토됩니다. 검토될 때 마다 ->의 좌측에 오는 가드가 계산되고, 그 값이 참이면 우측에 오는 식의 값이 계산되어 반환됩니다. 그런데 Erlang의 if는 C의 if와는 조금 다른 것이, C에서의 if는 else가 없어도 괜찮았지만 Erlang에서의 if는 그렇지 않다는 말입니다. 간단하게 말하면, Erlang의 if는 반드시 어떤 값을 반환해야 합니다. 그렇기 때문에 하나의 가드는 반드시 참으로 계산되어야 하고, 그 결과로 값을 반환해야 합니다. 위의 if 절 마지막에 true라는 가드를 둔 것은 그때문이죠. (C의 else에 해당합니다.) 그렇지 않으면 실행 도중에 예외가 발생합니다. if의 값을 계산할 수 없다고 투덜대면서 말이에요.

max(X, Y) ->
    Z = if
            (X>Y) -> X;
            true -> Y
        end,
    Z.

If가 값을 반환한다는 것이 놀랍게 느껴지는 분을 위해 예를 하나 써 봤습니다. 물론 이렇게 프로그램을 짤 일은 없겠지만 말이에요. 그럼 case는 어떤가요? case도 마찬가집니다.

max(X, Y) ->
    Z = case (X>Y) of
            true -> X;
            false -> Y
        end,
    Z.

그럼 case도 마치 else 있는 if 문처럼 동작해야 하겠군요? 맞습니다. :-)

제어문에 대한 글은 여기서 맺겠습니다. 중간에 이런 저런 업데이트를 많이 해서 '혹시나' 이 글을 읽으셨을 분들은 좀 헷갈리셨을 것 같군요. 죄송합니다.

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^