함수
함수 (Functions)
함수는 Erlang 코드를 묶는 기본 단위예요. 함수는 여러 개의 절(clause)로 나뉘어 정의되고, 각 절이 패턴 매칭과 가드(guard)를 통해 호출 시점을 결정하죠. 이번 장에서는 함수 선언 문법, 함수 호출이 평가되는 과정, 꼬리 재귀(tail recursion), 그리고 BIF(내장 함수)까지 차근차근 살펴볼게요.
함수 선언 문법 (Function Declaration Syntax)
함수 선언(function declaration) 은 세미콜론으로 구분된 함수 절들의 연속이며, 마지막은 마침표(.)로 끝나요. 함수 절(function clause) 은 절 헤드(clause head) 와 절 본문(clause body) 으로 이루어져 있고, 둘은 ->로 구분됩니다.
절 헤드 는 함수 이름, 인자 목록, 그리고 선택적으로 when 키워드로 시작하는 가드 시퀀스로 구성돼요:
Name(Pattern11,...,Pattern1N) [when GuardSeq1] ->
Body1;
...;
Name(PatternK1,...,PatternKN) [when GuardSeqK] ->
BodyK.
함수 이름은 원자(atom)고, 각 인자는 패턴이에요.
인자의 개수 N을 함수의 아리티(arity) 라고 해요. 함수는 모듈 이름, 함수 이름, 아리티로 유일하게 정의됩니다. 즉 같은 모듈에서 같은 이름을 가졌더라도 아리티가 다르면 서로 다른 함수예요.
모듈 mod 안에 아리티 N인 함수 f는 흔히 mod:f/N처럼 표기해요.
절 본문 은 쉼표(,)로 구분된 표현식들의 연속으로 이루어집니다:
Expr1,
...,
ExprN
유효한 Erlang 표현식과 가드 시퀀스는 Expressions에서 자세히 다뤄요.
예시:
fact(N) when N > 0 -> % first clause head
N * fact(N-1); % first clause body
fact(0) -> % second clause head
1. % second clause body
함수 평가 (Function Evaluation)
함수 M:F/N이 호출되면, 먼저 그 함수의 코드를 찾아요. 함수를 찾지 못하면 undef 런타임 오류가 발생합니다. 함수는 정의된 모듈 밖에서 보이려면 반드시 export 되어 있어야 한다는 점도 기억해 두세요.
함수를 찾았다면, 함수 절들은 다음 두 조건을 모두 만족하는 절을 찾을 때까지 순서대로 훑어봐요:
- 절 헤드의 패턴이 주어진 인자와 성공적으로 매칭되어야 한다.
- 가드 시퀀스가 있다면 그것이 참이어야 한다.
그런 절을 찾지 못하면 function_clause 런타임 오류가 발생합니다. 조건을 만족하는 절을 찾았다면, 그에 해당하는 절 본문이 평가돼요. 즉 본문의 표현식들이 순서대로 평가되고, 마지막 표현식의 값이 반환됩니다.
함수 fact를 생각해 볼게요:
-module(mod).
-export([fact/1]).
fact(N) when N > 0 ->
N * fact(N - 1);
fact(0) ->
1.
1의 팩토리얼을 계산하고 싶다고 가정해 봐요:
1> mod:fact(1).
평가는 첫 번째 절부터 시작됩니다. 패턴 N이 인자 1과 매칭되고, 매칭은 성공하며 가드(N > 0)도 참이에요. 그래서 N이 1로 바인딩되고 해당 본문이 평가돼요:
N * fact(N-1) => (N is bound to 1)
1 * fact(0)
이제 fact(0)이 호출되고, 함수 절을 다시 순서대로 훑어봐요. 먼저 패턴 N이 0과 매칭됩니다. 매칭은 성공하지만 가드(N > 0)는 거짓이에요. 다음으로 패턴 0이 인자 0과 매칭되고, 매칭이 성공해서 본문이 평가됩니다:
1 * fact(0) =>
1 * 1 =>
1
평가가 성공했고 mod:fact(1)이 1을 반환해요. 만약 mod:fact/1을 음수 인자로 호출하면 어떤 절 헤드도 매칭되지 않아서, function_clause 런타임 오류가 발생합니다.
꼬리 재귀 (Tail recursion)
함수 본문의 마지막 표현식이 함수 호출이라면, 꼬리 재귀 호출(tail-recursive call) 이 일어나요. 이렇게 하면 호출 스택 같은 시스템 자원이 소모되지 않게 보장할 수 있습니다. 즉 꼬리 재귀 호출로 만든 무한 루프는 호출 스택을 고갈시키지 않아서 (원칙적으로) 영원히 실행될 수 있어요.
예시:
loop(N) ->
io:format("~w~n", [N]),
loop(N+1).
앞서 본 팩토리얼 예시는 반대 사례예요. fact(N-1)에 대한 재귀 호출 결과에 곱셈을 수행하므로, 꼬리 재귀가 아닙니다.
내장 함수 (Built-In Functions, BIFs)
내장 함수(BIFs)는 런타임 시스템에서 C 코드로 구현되어 있어요. BIF는 Erlang으로 구현하기 어렵거나 불가능한 일을 처리하죠. 대부분의 BIF는 m:erlang 모듈에 속하지만, m:lists나 m:ets처럼 몇몇 다른 모듈에 속한 BIF도 있습니다.
m:erlang에 속한 가장 흔히 쓰이는 BIF들은 자동 import(auto-imported) 되어서, 모듈 이름을 붙이지 않아도 돼요. 어떤 BIF가 자동 import 되는지는 ERTS의 m:erlang 모듈에 명시되어 있습니다. 예를 들어 atom_to_list 같은 표준 타입 변환 BIF나 가드에서 허용되는 BIF들은 모듈 이름을 지정하지 않고 호출할 수 있어요.
예시:
1> tuple_size({a,b,c}).
3
2> atom_to_list('Erlang').
"Erlang"