함수 호출의 조작

함수 호출의 조작

함수 호출을 조작하는 방법을 살펴볼게요. sys.callmatch.call을 중심으로, 함수가 자신의 호출을 확인하고 인자를 정리하거나 재구성하는 기법을 다룬답니다.

출처: 문서

본문

6.5 함수 호출의 조작 (Manipulation of function calls)

함수는 sys.call의 결과를 살펴봄으로써 자기 자신이 어떻게 호출되었는지 알아낼 수 있어요. 다음 예시는 자기 자신의 호출을 그대로 돌려주는 함수예요.

> f <- function(x, y, ...) sys.call()
> f(y = 1, 2, z = 3, 4)
f(y = 1, 2, z = 3, 4)

하지만 실제로 이 방식은 디버깅 외에는 그다지 유용하지 않아요. 호출을 해석하려면 함수가 인자 매칭 과정을 계속 추적해야 하거든요. 예를 들어, 두 번째 실제 인자가 첫 번째 형식 인자(위 예시에서는 x)에 매칭된다는 사실을 함수가 알아야 해요.

더 자주 필요한 건 모든 실제 인자가 해당 형식 인자에 묶인(bound) 호출이에요. 이때 match.call 함수를 사용해요. 앞선 예시를 변형해서, 인자가 매칭된 자기 호출을 돌려주는 함수를 볼게요.

> f <- function(x, y, ...) match.call()
> f(y = 1, 2, z = 3, 4)
f(x = 2, y = 1, z = 3, 4)

이제 두 번째 인자가 x에 매칭되어 결과의 해당 위치에 나타나는 걸 확인할 수 있어요.

이 기법의 주된 용도는 같은 인자들로 다른 함수를 호출하되, 일부 인자를 삭제하거나 추가하기 위해서예요. 전형적인 사례는 lm 함수의 시작 부분에서 볼 수 있어요.

    mf <- cl <- match.call()
    mf$singular.ok <- mf$model <- mf$method <- NULL
    mf$x <- mf$y <- mf$qr <- mf$contrasts <- NULL
    mf$drop.unused.levels <- TRUE
    mf[[1]] <- as.name("model.frame")
    mf <- eval(mf, sys.frame(sys.parent()))

결과로 만들어진 호출은 부모 프레임(parent frame)에서 평가된다는 점에 주목하세요. 그래야 관련된 표현들이 타당한지 확신할 수 있거든요. 이 호출은 리스트 객체로 다룰 수 있는데, 첫 번째 원소는 함수 이름이고 나머지 원소들은 실제 인자 표현식들이에요. 각각에는 대응하는 형식 인자 이름이 태그로 붙어 있어요. 따라서 원하지 않는 인자를 제거하는 기법은 NULL을 할당하는 거예요(2, 3행에서 보이는 대로). 인자를 추가할 때는 태그가 있는 리스트 할당을 사용해요(4행에서 drop.unused.levels = TRUE를 전달하는 것처럼). 호출할 함수의 이름을 바꾸려면 리스트의 첫 번째 원소에 이름(name) 값을 할당하면 돼요. 여기서는 as.name("model.frame") 구문을 쓰거나 quote(model.frame)을 쓰면 돼요.

match.call 함수에는 expand.dots 인자가 있어요. 이 스위치를 FALSE로 설정하면 모든 ... 인자가 ... 태그를 가진 단일 인자로 모여요.

> f <- function(x, y, ...) match.call(expand.dots = FALSE)
> f(y = 1, 2, z = 3, 4)
f(x = 2, y = 1, ... = list(z = 3, 4))

... 인자는 리스트(정확히는 pairlist)이지 S에서처럼 list에 대한 호출이 아니에요.

> e1 <- f(y = 1, 2, z = 3, 4)$...
> e1
$z
[1] 3

[[2]]
[1] 4

이런 형태의 match.call을 사용하는 한 가지 이유는, 지정되지 않은 인자들을 그것들을 모를 수도 있는 함수에 그대로 전달하지 않기 위해 ... 인자들을 아예 제거하고 싶을 때예요. plot.formula에서 인용한 예시를 볼게요.

m <- match.call(expand.dots = FALSE)
m$... <- NULL
m[[1]] <- "model.frame"

더 정교한 응용은 update.default에 있어요. 여기서 선택적인 추가 인자들이 원래 호출의 인자들을 더하거나, 대체하거나, 취소할 수 있어요.

extras <- match.call(expand.dots = FALSE)$...
if (length(extras) > 0) {
    existing <- !is.na(match(names(extras), names(call)))
    for (a in names(extras)[existing]) call[[a]] <- extras[[a]]
    if (any(!existing)) {
        call <- c(as.list(call), extras[!existing])
        call <- as.call(call)
    }
}

extras[[a]] == NULL인 경우를 대비해 기존 인자들은 개별적으로 수정하는 데 주의를 기울인다는 점을 알 수 있어요. 보여준 것처럼 강제 변환(coercion) 없이는 호출 객체에 연결(concatenation)이 동작하지 않아요. 이는 아마도 버그라고 할 만해요.

함수 호출을 구성하기 위한 함수가 두 가지 더 있어요. 바로 calldo.call이에요. call 함수는 함수 이름과 인자 목록으로부터 호출 객체를 생성해요.

> x <- 10.5
> call("round", x)
round(10.5)

보시다시피 호출에는 x라는 심볼이 아니라 x의 값이 삽입돼요. 그래서 round(x)와는 확실히 달라요. 이 형태는 꽤 드물게 사용되지만, 함수 이름을 문자 변수로 가지고 있을 때 가끔 유용해요.

do.call 함수는 관련이 있지만 호출을 즉시 평가하고, 모든 인자를 담고 있는 모드 "list"의 객체에서 인자를 가져와요. 리스트나 데이터 프레임의 모든 원소에 cbind 같은 함수를 적용하고 싶을 때 자연스럽게 사용할 수 있어요.

is.na.data.frame <- function (x) {
    y <- do.call(cbind, lapply(x, is.na))
    rownames(y) <- row.names(x)
    y
}

다른 용도로는 do.call("f", list(...)) 같은 구문의 변형들이 있어요. 다만 이 방식은 실제 함수 호출 전에 인자들을 평가하기 때문에, 함수 내부의 지연 평가(lazy evaluation)나 인자 치환(argument substitution) 측면을 무력화할 수 있다는 점을 알아야 해요. call 함수에도 비슷한 주의가 적용돼요.

더 알아보기 (Learn more)

출처: R Language Definition - 함수 호출의 조작