함수

함수 (Functions)

함수는 Erlang 코드를 묶는 기본 단위예요. 함수는 여러 개의 절(clause)로 나뉘어 정의되고, 각 절이 패턴 매칭과 가드(guard)를 통해 호출 시점을 결정하죠. 이번 장에서는 함수 선언 문법, 함수 호출이 평가되는 과정, 꼬리 재귀(tail recursion), 그리고 BIF(내장 함수)까지 차근차근 살펴볼게요.

출처: Erlang 공식 문서 - Functions

함수 선언 문법 (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 되어 있어야 한다는 점도 기억해 두세요.

함수를 찾았다면, 함수 절들은 다음 두 조건을 모두 만족하는 절을 찾을 때까지 순서대로 훑어봐요:

  1. 절 헤드의 패턴이 주어진 인자와 성공적으로 매칭되어야 한다.
  2. 가드 시퀀스가 있다면 그것이 참이어야 한다.

그런 절을 찾지 못하면 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:listsm: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"

더 알아보기 (Learn more)