R语言中的右赋值运算符`->`是如何解析的?

87

这可能是一个琐碎的问题,但我无法回答它,也许答案会教我更多有关R如何工作的细节。

标题说了一切:R如何解析黑暗的右侧赋值函数->

我通常用来深入了解这个的技巧失败了:

`->`

错误:对象 -> 未找到

getAnywhere("->")

找不到名为->的对象

我们不能直接调用它:

`->`(3,x)

错误:找不到函数"->"

但是,当然它可以工作:

(3 -> x) #assigns the value 3 to the name x
# [1] 3

看起来 R 知道如何简单地翻转参数,但我认为上述方法肯定已经解决了这个问题:

pryr::ast(3 -> y)
# \- ()
#   \- `<- #R interpreter clearly flipped things around
#   \- `y  #  (by the time it gets to `ast`, at least...)
#   \-  3  #  (note: this is because `substitute(3 -> y)` 
#          #   already returns the reversed version)

将此与常规赋值运算符进行比较:

`<-`
.Primitive("<-")

`<-`(x, 3) #assigns the value 3 to the name x, as expected

?"->"?assignOpsR语言定义都只是简单地提及它作为正确的赋值运算符。

但显然->的使用有些独特。它不是函数/运算符(正如对getAnywhere的调用和直接对`->`的使用所展示的那样),那么它是什么?它完全属于自己的类别吗?

除了“->在R语言中被解释和处理的方式非常独特;记住并继续学习”之外,还有什么其他值得学习的东西吗?


2
实际上,这个相关的链接问题更加相关:https://dev59.com/M2Ag5IYBdhLWcg3wrMl5 - MichaelChirico
1
你只是给一个标签设置了一个值。这并不意味着它们是完全相同的。 - Ole Petersen
1个回答

81

首先声明一下,我对解析器的工作原理一窍不通。话虽如此,gram.y文件中的第296行定义了以下标记来表示R语言(YACC?)解析器中的赋值:

%token      LEFT_ASSIGN EQ_ASSIGN RIGHT_ASSIGN LBB

然后,在gram.c的5140到5150行, 这看起来像对应的C代码:
case '-':
  if (nextchar('>')) {
    if (nextchar('>')) {
      yylval = install_and_save2("<<-", "->>");
      return RIGHT_ASSIGN;
    }
    else {
      yylval = install_and_save2("<-", "->");
      return RIGHT_ASSIGN;
    }
  }

最后,在gram.c的第5044行开始,install_and_save2的定义:

/* Get an R symbol, and set different yytext.  Used for translation of -> to <-. ->> to <<- */
static SEXP install_and_save2(char * text, char * savetext)
{
    strcpy(yytext, savetext);
    return install(text);
}

再说一遍,对于没有使用解析器的经验来说,似乎->->>在解释过程中被直接翻译成<-<<-,分别处于一个非常低的层次。
你提出了一个非常好的观点,询问解析器如何“知道”将参数逆转为->——考虑到->似乎已安装到R符号表中作为<-,因此能够正确解释x -> yy <- x而不是x <- y。我所能做的就是在继续遇到支持我的主张的“证据”的同时提供进一步的猜测。希望有些慈悲的YACC专家会碰巧看到这个问题并提供一些见解;尽管如此,我不会抱太大期望。
回到gram.y的383和384行,这看起来像是与前面提到的LEFT_ASSIGNRIGHT_ASSIGN符号相关的一些解析逻辑:
|   expr LEFT_ASSIGN expr       { $$ = xxbinary($2,$1,$3);  setId( $$, @$); }
|   expr RIGHT_ASSIGN expr      { $$ = xxbinary($2,$3,$1);  setId( $$, @$); }

虽然我无法真正理解这个疯狂的语法,但我注意到xxbinary的第二个和第三个参数在WRT LEFT_ASSIGN (xxbinary($2,$1,$3))和RIGHT_ASSIGN (xxbinary($2,$3,$1))时会被交换。

这是我脑海中的想象:

LEFT_ASSIGN场景:y <- x

  • $2是上述表达式中解析器的第二“参数”,即<-
  • $1是第一个;即y
  • $3是第三个;即x

因此,结果(C?)调用将为xxbinary(<-, y, x)

将这个逻辑应用于RIGHT_ASSIGN,即x -> y,再加上我之前关于<-->被交换的猜想,

  • $2-> 被翻译成 <-
  • $1x
  • $3y

但由于结果是xxbinary($2,$3,$1)而不是xxbinary($2,$1,$3),所以结果仍然是xxbinary(<-, y, x)


进一步扩展这个话题,我们在gram.c的3310行中定义了xxbinary
static SEXP xxbinary(SEXP n1, SEXP n2, SEXP n3)
{
    SEXP ans;
    if (GenerateCode)
    PROTECT(ans = lang3(n1, n2, n3));
    else
    PROTECT(ans = R_NilValue);
    UNPROTECT_PTR(n2);
    UNPROTECT_PTR(n3);
    return ans;
}

很不幸,我在 R 源代码中找不到lang3(或其变体lang1lang2等)的确切定义,但我认为它用于以与解释器同步的方式评估特殊函数(即符号)。


更新 我会尽我所能回答评论中的一些额外问题,但考虑到我对解析过程的了解非常有限,可能无法给出最佳答案。

1)这真的是R中唯一表现出这种行为的对象吗?(我想起了John Chambers通过Hadley的书所引用的话:“存在的每一件事都是一个对象。发生的每一件事都是一个函数调用。”这显然超出了那个范畴——还有其他像这样的东西吗?

首先,我同意这超出了那个范畴。我认为 Chambers 的这句话涉及 R 环境,即在此低级解析阶段之后发生的所有过程。不过稍后我会更详细地讨论这个问题。无论如何,我能找到的唯一另一个展现这种行为的例子是 ** 运算符,它是更常见的指数运算符 ^ 的同义词。和右赋值一样,解释器似乎没有将 ** 视为函数调用等,

R> `->`
#Error: object '->' not found
R> `**`
#Error: object '**' not found 

我发现这个是因为它是唯一一个被C解析器使用的install_and_save2的情况:

case '*':
  /* Replace ** by ^.  This has been here since 1998, but is
     undocumented (at least in the obvious places).  It is in
     the index of the Blue Book with a reference to p. 431, the
     help for 'Deprecated'.  S-PLUS 6.2 still allowed this, so
     presumably it was for compatibility with S. */
  if (nextchar('*')) {
    yylval = install_and_save2("^", "**");
    return '^';
  } else
    yylval = install_and_save("*");
return c;

2) 这是在什么时候发生的?我想到substitute(3 -> y)已经翻转了表达式;我无法从源代码中找出substitute是如何触发YACC的...

当然,我这里还在推测,但是是的,我认为我们可以安全地假设,当你调用substitute(3 -> y)时,从替换函数的角度来看,表达式一直都是y <- 3;例如,该函数完全不知道你输入了3 -> y。像R使用的99%的C函数一样,do_substitute只处理SEXP参数——在3 -> y(== y <- 3)的情况下是EXPRSXP。这就是我上面提到的R环境和解析过程之间的区别。我认为没有任何特定的触发器会引起解析器开始工作,而是所有你输入到解释器中的内容都会被解析。昨晚我对YACC/Bison解析器生成器进行了一些阅读,据我所知(也就是说,不要把所有赌注都压在这上面),Bison使用你定义的语法(在.y文件中)来生成一个C解析器——即一个实际执行输入解析的C函数。反过来,你在R会话中输入的所有内容都首先由这个C解析函数处理,然后将适当的操作委托给R环境(顺便说一下,我非常宽泛地使用了这个术语)。在此阶段,lhs -> rhs将被翻译为rhs <- lhs**将被翻译为^等等...例如,这是names.c中原始函数表的摘录:

/* Language Related Constructs */

/* Primitives */
{"if",      do_if,      0,  200,    -1, {PP_IF,      PREC_FN,     1}},
{"while",   do_while,   0,  100,    2,  {PP_WHILE,   PREC_FN,     0}},
{"for",     do_for,     0,  100,    3,  {PP_FOR,     PREC_FN,     0}},
{"repeat",  do_repeat,  0,  100,    1,  {PP_REPEAT,  PREC_FN,     0}},
{"break",   do_break, CTXT_BREAK,   0,  0,  {PP_BREAK,   PREC_FN,     0}},
{"next",    do_break, CTXT_NEXT,    0,  0,  {PP_NEXT,    PREC_FN,     0}},
{"return",  do_return,  0,  0,  -1, {PP_RETURN,  PREC_FN,     0}},
{"function",    do_function,    0,  0,  -1, {PP_FUNCTION,PREC_FN,     0}},
{"<-",      do_set,     1,  100,    -1, {PP_ASSIGN,  PREC_LEFT,   1}},
{"=",       do_set,     3,  100,    -1, {PP_ASSIGN,  PREC_EQ,     1}},
{"<<-",     do_set,     2,  100,    -1, {PP_ASSIGN2, PREC_LEFT,   1}},
{"{",       do_begin,   0,  200,    -1, {PP_CURLY,   PREC_FN,     0}},
{"(",       do_paren,   0,  1,  1,  {PP_PAREN,   PREC_FN,     0}},

你会注意到,->->>**在这里没有定义。据我所知,R原始表达式如<-[等是R环境与任何底层C代码之间最接近的交互。我的建议是,在这个过程的这个阶段(从你在解释器中输入一组字符并按下“Enter”键,直到实际评估有效的R表达式),解析器已经完成了它的工作,这就是为什么你不能通过用反引号将它们括起来来获得->**的函数定义,就像你通常可以做的那样。

20
与此同时,我敢说这个回答值得一枚“克拉默奖章”(注:该奖项类似于谷歌的“蓝色勋章”)。好了,我应该认真回到工作中了... - MichaelChirico
2
仅供记录(同时作为完全的解析新手)我想指出,似乎有一个区别在一个标记的类型(这里为RIGHT_ASSIGN)和其值(这里为<-,由install_and_save2分配给了yylval)之间。对我来说,类型用于指导表达式的解析(将我们发送到读取{ $$ = xxbinary($2,$3,$1); setId( $$, @$); }的分支),而它的是通过xxbinary的第一个参数(即$2)传递的。 - Josh O'Brien
@Josh O'Brien 感谢您的建议(以及编辑);表面上看,这听起来很合理。如果您有兴趣,请随时将该信息或任何其他相关信息添加到我的答案中(恐怕如果我试图自己表达,我会搞砸解释)。 - nrussell
3
@nrussell 不客气。 lang3 等都是内联函数,并且可以在 这里,即 $RHOME/src/include/Rinlinedfuns.h 找到。在我看来,它们在这里的作用是将各个标记和解析表达式粘合在一起,形成类似列表的语言对象,逐步构建输入表达式的完全解析版本。 - Josh O'Brien
1
感谢更新!至于 **,我确实记得在某个地方读到过这个运算符有点像退化的东西,所以至少我曾经见过它被认为是一种异类。无论如何,我的实用程序现在充满了可疑的有用知识...这正是我喜欢的! - MichaelChirico

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接