メモ:RのREPLのコードを追ってみる

AtsushiさんのQiita記事を読んでちょっと興味が湧いたので調べたことをメモ。

記事にあった問題をわかりやすくすると、こんな感じ。

print.function <- function(x, ...) {
  base::print.default(deparse(substitute(x)))
}

# printを直接呼ぶとxはちゃんと関数名にsubstitute()される
print(max)
#> [1] "max"

# REPL越しにprintを呼ぶとxになる
max
#> [1] "x"

Rのコードを追ったところ、呼び出し側の環境でxという名前で引数を渡しているからみたいです。REPLの実装はこのあたりです。

特に注目はこのへん。eval()でコードを評価して、R_visibletrueだったらPrintValueEnv()という関数を呼んで結果を表示するっぽいです。

 PROTECT(value = eval(thisExpr, rho));
    SET_SYMVALUE(R_LastvalueSymbol, value);
    wasDisplayed = R_Visible;
    if (R_Visible)
        PrintValueEnv(value, rho);

その実装はこのあたり。

なんか書いてあります。

 /* Bind value to a variable in a local environment, similar to
      a local({ x <- <value>; print(x) }) call. This avoids
      problems in previous approaches with value duplication and
      evaluating the value, which might be a call object. */
    PROTECT(call = lang2(prinfun, xsym));
    PROTECT(env = NewEnvironment(R_NilValue, R_NilValue, env));
    defineVar(xsym, s, env);
    eval(call, env);
    defineVar(xsym, R_NilValue, env); /* to eliminate reference to s */

ということで、xが返ってくるのは、print()の引数がxだからではなく、RのREPLが新しい環境をつくってそこで値をxという変数に入れてからprint()に渡すから、ということのようです。 これはお手上げ感ありますが、/* to eliminate reference to s */とあるので、その実体を参照している変数の名前を取る方法があればあるいは...?

難しそうなのでこの辺でギブアップ。

余談

ちなみに、メソッドディスパッチは元の引数がそのまま渡されるっぽいので今回は違いましたが、以下のように、関数呼び出しがひとつ挟まるので元の引数が取れない、みたいなことがあります。

fun_outside <- function(x) {
  fun_inside(x)
}

fun_inside <- function(x) {
  base::print.default(deparse(substitute(x)))
}

fun_outside(max)
#> [1] "x"

こういう問題であれば、呼び出し側の環境でsubstitute()するという手があります。 substitute()にはenvという引数が用意されているので、ここにparent.frame()で呼び出し環境を指定します。

fun_inside <- function(x) {
  base::print.default(deparse(substitute(x, parent.frame())))
}

fun_outside(max)
#> [1] "max"

再帰的に環境を駆け上がっていく方法もあるのかな…?