4장 Functions — 함수
4장 Functions — 함수
R에서 함수는 그냥 값을 계산해 주는 도구만이 아니라, 'R 그 자체를 확장하는 방법'이에요. 이 장에서는 함수를 어떻게 작성하고, R이 함수를 어떤 객체로 보고 어떻게 불러와서 평가하는지까지 — 함수라는 개념의 처음부터 끝까지를 차근차근 짚어볼게요.
출처: R 공식 매뉴얼
본문
4.1 함수 작성하기 (Writing functions)
R은 데이터 분석 도구로서 아주 유용하지만, 대부분의 사용자는 금방 '나만의 함수'를 작성하고 싶어 하게 돼요. 그게 바로 R의 진짜 강점 중 하나예요. 사용자가 직접 프로그래밍할 수 있고, 원하면 시스템 차원의 함수를 자신에게 더 맞는 함수로 바꿔치기도 할 수 있거든요.
R은 또 여러분이 만든 함수를 문서화하기 쉽게 해 주는 기능도 제공해요. 이에 대해서는 Writing R Extensions의 "Writing R documentation" 항목을 참고하면 돼요.
4.1.1 문법과 예제 (Syntax and examples)
함수를 작성하는 문법은 이렇게 생겼어요.
function ( arglist ) body
함수 선언에서 가장 먼저 나오는 구성 요소가 바로 function이라는 키워드인데, 이 키워드는 R에게 "지금부터 함수를 만들 거야"라고 알려주는 역할을 해요.
인자 목록(argument list) 은 형식 인자(formal arguments)들을 쉼표로 구분해 나열한 목록이에요. 형식 인자는 기호(symbol)일 수도 있고, symbol = expression 형태의 문장일 수도 있으며, 특별한 형식 인자인 ...일 수도 있어요.
함수 몸통(body) 은 유효한 R 표현식이라면 무엇이든 될 수 있어요. 보통은 중괄호({와 })로 묶은 표현식들의 묶음으로 작성하죠.
함수는 대체로 어떤 기호에 할당돼서 사용되지만, 꼭 그럴 필요는 없어요. function을 호출했을 때 돌아오는 값 자체가 바로 함수예요. 이 값에 이름을 붙이지 않으면 이를 익명 함수(anonymous function) 라고 불러요. 익명 함수는 주로 다른 함수의 인자로 전달될 때 가장 많이 쓰이는데, apply 계열이나 outer 같은 함수의 인자로 넣는 경우가 대표적이에요.
간단한 예시 하나 볼게요. echo <- function(x) print(x)처럼 정의하면, echo는 단일 인자를 받아서 그 인자를 그대로 출력하는 함수가 돼요. echo를 호출하면 인자가 화면에 찍히는 식이죠.
4.1.2 인자 (Arguments)
함수의 형식 인자들은 함수를 호출할 때 값이 채워질 변수들을 정의해요. 이 인자들의 이름은 함수 몸통 안에서도 그대로 쓸 수 있는데, 그 자리에서 호출 시점에 공급된 값을 얻게 돼요.
인자의 기본값은 name = expression이라는 특수한 형태로 지정할 수 있어요. 이 경우, 함수를 호출할 때 사용자가 그 인자에 값을 주지 않으면 그 표현식(expression)이 해당 기호에 결합돼요. 그리고 실제로 값이 필요해질 때, 그 표현식은 함수의 평가 프레임(evaluation frame) 안에서 평가돼요.
기본 동작을 지정하는 또 다른 방법은 missing 함수를 쓰는 거예요. missing에 형식 인자의 이름을 넘겨서 호출하면, 그 형식 인자가 어떤 실인자(actual argument)에도 매칭되지 않았고 몸통 안에서 이후에 수정되지도 않았을 때 TRUE를 돌려줘요. 즉 인자가 "비어 있다(missing)"면 기본 동작이 적용되는 식이죠. 참고로 missing은 인자를 강제로 평가하지 않아요.
특별한 형태의 인자인 ...는 개수 제한 없이 얼마든지 많은 인자를 받을 수 있어요. 이 인자는 여러 가지 목적으로 쓰이는데, 우선 임의의 개수의 인자를 받는 함수를 작성할 수 있게 해 주고, 또 어떤 인자들을 중간 함수에 흡수시켰다가 이후에 호출되는 함수들이 그걸 다시 꺼내 쓸 수 있게 해 주죠.
4.2 함수는 객체다 (Functions as objects)
R에서 함수는 일급 객체(first class objects) 예요. 즉 R 객체가 필요한 어떤 자리에서든 함수를 쓸 수 있어요. 특히 함수를 다른 함수의 인자로 넘길 수도 있고, 함수가 함수를 값으로 돌려줄 수도 있다는 뜻이에요. 자세한 내용은 "Function objects" 항목을 참고하면 돼요.
4.3 평가 (Evaluation)
4.3.1 평가 환경 (Evaluation environment)
함수가 호출되면 새로운 평가 프레임(evaluation frame) 이 만들어져요. 이 프레임 안에서 형식 인자들이 "Argument matching"에서 설명하는 규칙에 따라 공급된 인자들과 매칭되고, 함수 몸통의 문장들은 이 환경 프레임 안에서 순서대로 평가돼요.
이 평가 프레임의 enclosing frame(감싸는 프레임) 은 호출된 함수에 결합된 환경 프레임이에요. 이 점이 S와 다를 수 있는 부분이에요. 많은 함수가 자기 환경으로 .GlobalEnv를 갖고 있지만 꼭 그래야 하는 건 아니고, 네임스페이스(namespace)를 가진 패키지에 정의된 함수는 (보통) 자기 패키지의 네임스페이스를 환경으로 갖게 돼요.
4.3.2 인자 매칭 (Argument matching)
이 절은 클로저(closure)에 적용되는 내용이고 기본형 함수(primitive functions)에는 해당하지 않아요. 기본형 함수는 대개 태그를 무시하고 위치 기반 매칭을 하는데, log, round, signif, rep, seq.int 등은 예외가 있으니 각자 도움말 페이지를 확인해야 해요.
함수 평가에서 가장 먼저 일어나는 일은 형식 인자를 실인자(actual, 즉 공급된 인자)와 매칭하는 것이에요. 이 매칭은 세 단계 패스로 진행돼요.
-
태그에 대한 정확 매칭(Exact matching on tags). 이름이 붙은 각 공급 인자에 대해, 형식 인자 목록에서 이름이 정확히 일치하는 항목을 찾아요. 같은 형식 인자가 여러 실인자와 매칭되거나 그 반대가 되는 건 오류예요.
-
태그에 대한 부분 매칭(Partial matching on tags). 남은 이름 있는 공급 인자들을 남은 형식 인자들과 부분 매칭으로 비교해요. 공급 인자의 이름이 어떤 형식 인자 이름의 앞부분과 정확히 일치하면 두 인자는 매칭된 것으로 간주돼요. 부분 매칭이 여러 개 생기면 오류예요. 예를 들어
f <- function(fumble, fooey) fbody일 때,f(f = 1, fo = 2)는 두 번째 실인자가fooey와만 매칭된다 해도 불법이에요. 하지만f(f = 1, fooey = 2)는 두 번째 인자가 정확히 매칭돼 부분 매칭 후보에서 빠지므로 합법이에요. 형식 인자에...가 포함돼 있으면, 부분 매칭은...앞에 오는 인자에만 적용돼요. -
위치 매칭(Positional matching). 매칭되지 않고 남은 형식 인자들은 이름 없는 공급 인자들과 순서대로 결합돼요.
...인자가 있으면 나머지 인자들을 태그 있든 없든 전부 받아들이죠.
그래도 매칭되지 않고 남는 인자가 있으면 오류가 선언돼요.
인자 매칭은 match.arg, match.call, match.fun 함수로 보완·활용할 수 있어요. R이 쓰는 부분 매칭 알고리즘에 직접 접근하려면 pmatch를 쓰면 돼요.
4.3.3 인자 평가 (Argument evaluation)
함수 인자의 평가에 대해 가장 중요하게 알아둘 점 중 하나는, 공급된 인자와 기본 인자가 다르게 취급된다는 사실이에요. 함수에 공급된 인자는 호출한 함수의 평가 프레임에서 평가되고, 기본 인자는 그 함수 자신의 평가 프레임에서 평가돼요.
R에서 함수를 호출하는 의미론은 call-by-value(값에 의한 호출) 예요. 일반적으로 공급된 인자는 '공급된 값으로 초기화된 로컬 변수'처럼 동작해요. 함수 안에서 공급된 인자의 값을 바꿔도, 호출한 프레임의 변수 값에는 영향이 없어요.
R은 함수 인자에 대해 지연 평가(lazy evaluation) 의 한 형태를 써요. 즉 인자는 필요할 때까지 평가되지 않아요. 그리고 어떤 경우에는 인자가 영영 평가되지 않을 수도 있다는 걸 알아둬야 해요. 그래서 인자를 이용해 부작용(side-effect)을 일으키는 건 나쁜 스타일이에요. C에서는 foo(x = y)처럼 쓰면 foo를 y의 값으로 호출하면서 동시에 y의 값을 x에 대입하는 흔한 패턴이지만, R에서는 그렇게 쓰면 안 돼요. 그 인자가 평가된다는 보장이 없으니, 대입이 이뤄지지 않을 수 있기 때문이에요.
또 하나 짚어둘 점은, foo(x <- y)처럼 썼을 때 인자가 평가된다면 그 효과는 foo의 평가 환경이 아니라 호출 환경의 x 값을 바꾸는 것이에요.
함수 안에서 인자로 쓰인 실제(기본값이 아닌) 표현식에 접근하는 것도 가능해요. 그 메커니즘은 프로미스(promise) 로 구현돼요. 함수가 평가될 때 인자로 쓰인 실제 표현식은 프로미스에 저장되는데, 그와 함께 함수가 호출된 환경을 가리키는 포인터도 함께 저장돼요. (만약) 그 인자가 평가되면, 저장된 표현식은 함수가 호출된 환경에서 평가돼요. 환경 포인터만 쓰기 때문에, 그 환경에 어떤 변경이 가해져도 이 평가 동안 그 변경이 반영돼요. 그리고 그 결과값은 프로미스의 별도 공간에 저장돼요. 이후의 평가들은 이 저장된 값을 꺼내 쓰죠 (두 번째 평가는 일어나지 않아요). 평가되지 않은 표현식에 접근하는 데는 substitute를 쓸 수도 있어요.
함수가 호출되면 각 형식 인자에는 호출의 로컬 환경 안에서 프로미스가 하나씩 할당돼요. 이때 표현식 자리에 실인자(있으면)가, 환경 자리에는 호출자의 환경이 담겨요. 호출에서 어떤 형식 인자에 실인자가 주어지지 않고 기본 표현식이 있는 경우에도 마찬가지로 형식 인자의 표현식 자리에 할당되는데, 다만 이때 환경은 로컬 환경으로 설정돼요.
프로미스의 표현식 자리 내용을 그 프로미스의 환경에서 평가해 값 자리를 채우는 과정을 프로미스 강제하기(forcing the promise) 라고 해요. 프로미스는 딱 한 번만 강제되고, 그 뒤로는 값 자리의 내용이 그대로 사용돼요.
프로미스는 그 값이 필요해질 때 강제돼요. 보통은 내부 함수 안에서 강제되지만, 프로미스 자체를 직접 평가해 강제할 수도 있어요. 이는 기본 표현식이 다른 형식 인자나 로컬 환경의 다른 변수 값에 의존할 때 종종 유용해요. 아래 예시에서 label이 다음 줄에서 x가 바뀌기 전의 값을 기준으로 만들어지도록 보장하는 걸 볼 수 있어요.
function(x, label = deparse(x)) {
label
x <- x + 1
print(label)
}
4.3.4 스코프 (Scope)
스코프, 즉 스코핑 규칙은 간단히 말해 평가기가 어떤 기호(symbol)의 값을 찾을 때 쓰는 규칙의 집합이에요. 모든 프로그래밍 언어에는 이런 규칙이 있죠. R의 규칙은 꽤 단순한 편이지만, 기본(기본값) 규칙을 우회할 수 있는 메커니즘도 존재해요.
R은 어휘적 스코프(lexical scope) 라고 불리는 규칙 집합을 따르는데, 이는 표현식이 만들어진 시점에 적용 중이던 변수 바인딩들이 그 표현식 안의 바인딩되지 않은 기호들에 값을 제공한다는 의미예요.
스코프의 흥미로운 성질 대부분은 함수 평가와 얽혀 있으니 여기에 집중해 볼게요. 기호는 바인딩되거나(bound) 바인딩되지 않은(unbound) 둘 중 하나예요. 함수의 모든 형식 인자는 함수 몸통 안에서 바인딩된 기호를 제공해요. 몸통 안의 그 외의 기호들은 로컬 변수이거나 바인딩되지 않은 변수예요. 로컬 변수는 함수 안에서 정의된 변수인데, R에는 변수에 대한 형식적인 정의가 없어 '필요할 때 그냥 쓰는' 방식이라 어떤 변수가 로컬인지 아닌지 판단하기 어렵기도 해요. 로컬 변수는 반드시 먼저 정의돼야 하는데, 보통 대입의 왼쪽에 등장하는 방식으로 정의돼요.
평가 과정에서 바인딩되지 않은 기호를 만나면 R은 그 값을 찾으려 시도하고, 이때 스코핑 규칙이 과정을 좌우해요. R에서는 그 함수의 환경을 먼저 탐색하고, 그다음 그 enclosure(감싸는 환경), 이렇게 전역 환경(global environment)에 도달할 때까지 계속 올라가요. 전역 환경은 순차적으로 탐색되는 환경들의 검색 리스트를 이끄는데, 처음으로 일치하는 기호를 찾은 값을 사용해요.
이 규칙들이 '함수가 다른 함수의 값으로 반환될 수 있다'는 사실과 결합되면, 언뜻 보기엔 특이해 보이지만 실제로는 꽤 멋진 성질들이 나와요. 간단한 예시를 볼게요.
f <- function() {
y <- 10
g <- function(x) x + y
return(g)
}
h <- f()
h(3)
여기서 흥미로운 질문은 h를 평가하면 무슨 일이 벌어지느냐예요. 함수 몸통을 평가할 때 로컬 변수나 바인딩된 변수의 값을 정하는 건 문제없어요. 스코핑 규칙은 바로 바인딩되지 않은 변수의 값을 언어가 어떻게 찾는지를 결정하죠.
h(3)을 평가하면 그 몸통이 g의 몸통이라는 걸 확인할 수 있어요. 그 몸통 안에서 x는 형식 인자에 바인딩돼 있고, y는 바인딩돼 있지 않아요. 어휘적 스코프를 쓰는 언어에서는 x가 값 3과 결합되고, y는 f에 로컬한 값 10과 결합되므로 h(3)은 13을 돌려줘야 해요. R에서 실제로 그렇게 동작해요.
한편 S에서는 스코핑 규칙이 달라서, 워크스페이스에 y라는 변수가 있으면 그 값을 쓰고, 없다면 y를 찾을 수 없다는 오류를 내게 돼요.
더 알아보기
- 함수와 관련된 핵심 명령어:
function,missing,match.arg,match.call,match.fun,pmatch,substitute,return - 함수를 값으로 다루는 기법과 함께 보면 좋은 부분: R의 "Function objects" 항목
- 함수 정의 문서화 방법: Writing R Extensions의 "Writing R documentation"