치환
치환 (Substitutions)
R에서 표현식의 내용을 치환하는 substitute와 bquote의 동작 방식을 설명할게요.
출처: 문서
본문
6.2 치환
사실 이전 절에서처럼 표현식의 내부를 수정하고 싶은 경우는 그리 흔하지 않아요. 더 자주 필요한 것은, 예를 들어 플롯에 라벨을 달기 위해 표현식을 가져와서 deparse 하는 정도죠. plot.default의 시작 부분에서 그 예를 볼 수 있어요.
xlabel <- if (!missing(x))
deparse(substitute(x))
이렇게 하면 나중에 x축 라벨을 붙일 때 plot의 x 인자로 전달된 변수나 표현식이 사용돼요. 이를 위해 사용하는 함수가 바로 substitute예요. substitute는 표현식 x를 받아서 형식 인자(formal argument) x로 전달된 표현식으로 치환해요. 여기서 중요한 점은, 이런 일이 일어나려면 x가 자신의 값을 만들어낸 표현식에 대한 정보를 담고 있어야 한다는 거예요. 이는 R의 지연 평가(lazy evaluation) 방식과 관련이 있어요(promise 객체 참고). 형식 인자는 사실 promise 객체예요. promise는 세 개의 슬롯을 가진 객체인데, 하나는 그 promise를 정의하는 표현식, 하나는 그 표현식을 평가할 환경, 나머지 하나는 평가된 후의 값이에요. substitute는 promise 변수를 알아보고 그 표현식 슬롯의 값으로 치환해요. 만약 substitute가 함수 내부에서 호출되면, 함수의 지역 변수들도 치환 대상이 돼요. substitute의 인자는 단순한 식별자일 필요 없어요. 여러 변수를 포함한 표현식일 수도 있고, 그 변수들 각각에 대해 치환이 일어나요. 또한 substitute에는 변수를 찾아볼 환경이나 리스트를 지정하는 추가 인자도 있어요. 예를 들어:
> substitute(a + b, list(a = 1, b = quote(x)))
1 + x
x를 치환하려면 quote가 필요하다는 점에 주의하세요. 이런 방식은 그래프에 수식을 넣는 기능과 함께 쓸 때 유용해요. 다음 예를 보면 알 수 있어요.
> plot(0)
> for (i in 1:4)
+ text(1, 0.2 * i,
+ substitute(x[ix] == y, list(ix = i, y = pnorm(i))))
여기서 중요한 건 치환이 순전히 어휘적(lexical)이라는 점이에요. 결과로 만들어진 호출 객체를 실제로 평가했을 때 말이 되는지는 전혀 검사하지 않아요. substitute(x <- x + 1, list(x = 2))는 아무 문제 없이 2 <- 2 + 1을 반환해요. 하지만 R의 일부 기능들은 무엇이 말이 되고 안 되는지에 대해 나름의 규칙을 정해두고 있어서, 이런 비정상적인 표현식도 실제로 쓸모가 있을 수 있어요. 예를 들어 '그래프에 수식 넣기' 기능을 사용할 때는 문법적으로는 맞지만 평가하면 의미가 없는 구성, 이를테면 '{}>=40*" years"' 같은 것을 자주 쓰게 돼요.
substitute는 첫 번째 인자를 평가하지 않아요. 그래서 변수에 들어 있는 객체에 대해 치환을 하려면 어떻게 해야 하는지가 문제가 되죠. 해결책은 substitute를 한 번 더 사용하는 거예요. 이렇게요:
> expr <- quote(x + y)
> substitute(substitute(e, list(x = 3)), list(e = expr))
substitute(x + y, list(x = 3))
> eval(substitute(substitute(e, list(x = 3)), list(e = expr)))
3 + y
치환에 대한 정확한 규칙은 다음과 같아요. 첫 번째 인자의 파스 트리(parse tree)에 있는 각 기호(symbol)를 두 번째 인자와 대조해요. 두 번째 인자는 태그가 붙은 리스트(tagged list)나 환경 프레임(environment frame)일 수 있어요. 그 기호가 단순한 지역 객체라면 그 값이 삽입돼요. 단, 전역 환경(global environment)과 대조할 때는 예외예요. 기호가 promise라면(보통 함수 인자), promise의 표현식이 치환돼요. 기호가 대조되지 않으면 그대로 남아요. 최상위 레벨에서 치환할 때의 이 특별한 예외는 확실히 독특해요. 이는 S에서 물려받은 것인데, 그 이유는 아마도 최상위 레벨에서는 어떤 변수가 바인딩되어 있을지 통제할 수 없기 때문에, 차라리 substitute가 quote처럼 동작하게 만드는 게 낫다는 판단이었을 거예요.
promise 치환 규칙은 지역 변수가 substitute를 사용하기 전에 수정된 경우 S와 약간 달라요. R은 그 변수의 새 값을 사용하는 반면, S는 무조건 인자 표현식을 사용해요. 단, 상수인 경우는 예외인데, 그 때문에 S에서는 f((1))과 f(1)이 매우 다르게 동작할 수 있는 흥미로운 결과가 생겨요. R의 규칙이 훨씬 깔끔하지만, 지연 평가와 관련해서 일부 사람들에게는 의외로 다가오는 결과가 있어요. 다음을 봐요.
logplot <- function(y, ylab = deparse(substitute(y))) {
y <- log(y)
plot(y, ylab = ylab)
}
이건 단순해 보이지만, 실제로 해보면 y 라벨이 지저분한 c(...) 표현식이 되는 걸 발견하게 돼요. 지연 평가 규칙 때문에 ylab 표현식이 평가되는 시점이 y가 수정된 후가 되어서 그런 거예요. 해결책은 ylab이 먼저 평가되도록 강제하는 거예요. 즉,
logplot <- function(y, ylab = deparse(substitute(y))) {
ylab
y <- log(y)
plot(y, ylab = ylab)
}
이 상황에서 eval(ylab)을 사용하면 안 된다는 점에 주의하세요. ylab이 언어 객체나 표현식 객체라면, eval을 쓰면 그 객체 자체도 평가되어 버려요. quote(log[e](y)) 같은 수식을 전달하는 경우라면 전혀 바람직하지 않겠죠.
substitute의 변형으로 bquote가 있어요. bquote는 일부 하위 표현식을 해당 값으로 바꾸는 데 사용해요. 아까 위의 예제는
> plot(0)
> for (i in 1:4)
+ text(1, 0.2 * i,
+ substitute(x[ix] == y, list(ix = i, y = pnorm(i))))
다음처럼 더 간결하게 작성할 수 있어요.
plot(0)
for(i in 1:4)
text(1, 0.2*i, bquote( x[.(i)] == .(pnorm(i)) ))
이 표현식은 기본적으로 인용(quote)되지만, .() 하위 표현식의 내용은 예외로, 그 값으로 대체돼요. 값을 다른 환경에서 계산하도록 하는 선택적 인자도 있어요. bquote의 문법은 LISP의 백쿼트(backquote) 매크로에서 가져온 거예요.