R 究竟如何解析“->”(右赋值运算符)?

How exactly does R parse `->`, the right-assignment operator?

提问人:MichaelChirico 提问时间:1/5/2016 最后编辑:MichaelChirico 更新时间:1/17/2017 访问量:2282

问:

所以这是一个微不足道的问题,但我无法回答它让我很困扰,也许答案会教我更多关于 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

?"->"、 和 R 语言定义都只是顺便提到它作为正确的赋值运算符。?assignOps

但显然,使用方式有一些独特之处。它不是一个函数/运算符(正如对 和 to 的调用所表明的那样),那么它是什么?它是否完全属于自己的类别?->getAnywhere`->`

除了“在R语言中解释和处理的方式完全独特之外,还有什么可以学习的吗?记住并继续前进“?->

R YACC公司

评论

2赞 MichaelChirico 1/5/2016
实际上,这个相关的相关问题更为相关:stackoverflow.com/questions/23309687/......
1赞 Ole Petersen 1/5/2016
您只需将标签设置为一个值即可。这并不意味着它们是相同的

答:

81赞 nrussell 1/5/2016 #1

首先,我要说的是,我对解析器的工作原理一无所知。话虽如此,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 符号表中 - 因此能够正确地解释为 和 不是 .我能做的最好的事情就是在我继续遇到“证据”来支持我的主张时提供进一步的推测。希望一些仁慈的 YACC 专家会偶然发现这个问题并提供一些见解;不过,我不会对此屏住呼吸。->-><-x -> yy <- xx <- y

回到 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( $$, @$); }

虽然我无法真正理解这种疯狂语法的正面或反面,但我确实注意到第二个和第三个参数被交换为 WRT () 和 ()。xxbinaryLEFT_ASSIGNxxbinary($2,$1,$3)RIGHT_ASSIGNxxbinary($2,$3,$1)

这是我脑海中描绘的:

LEFT_ASSIGN场景:y <- x

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

因此,生成的 (C?) 调用将是 。xxbinary(<-, y, x)

将这个逻辑应用于 ,即 ,结合我之前关于和被交换的猜想,RIGHT_ASSIGNx -> 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 源代码中找不到(或其变体等)的正确定义,但我假设它用于以与解释器同步的方式评估特殊函数(即符号)。lang3lang1lang2


更新鉴于我对解析过程的(非常)有限的了解,我将尽我所能在评论中解决您的一些其他问题。

1)这真的是R中唯一一个行为这样的对象吗?(我有 想起了约翰·钱伯斯(John Chambers)在哈德利(Hadley)的书中引用的一句话:“一切 存在是一个对象。发生的一切都是函数调用。 这显然超出了这个领域——还有别的类似的东西吗 这?

首先,我同意这超出了该领域。我相信钱伯斯的引述涉及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) 这究竟是什么时候发生的?我想到了替代品(3 -> y) 已经翻转了表达式;我无法从源头上弄清楚什么替代品会ping通YACC......

当然,我仍然在这里推测,但是是的,我认为我们可以有把握地假设,当你调用 时,从代入函数的角度来看,表达式总是 ;例如,该函数完全不知道您键入了 。,就像 R 使用的 99% 的 C 函数一样,只处理参数 - 我相信 (==) 的情况下。这就是我在上面区分 R 环境和解析过程时所提到的。我不认为有什么东西可以具体触发解析器开始行动 - 而是你输入解释器的所有内容都会被解析。昨晚我又读了一点关于 YACC / Bison 解析器生成器的文章,据我了解(也就是不要把农场押在这上面),Bison 使用您定义的语法(在文件中)在 C 中生成一个解析器 - 即对输入进行实际解析的 C 函数。反过来,您在 R 会话中输入的所有内容首先由此 C 解析函数处理,然后委托要在 R 环境中执行的相应操作(顺便说一下,我非常松散地使用这个术语)。在此阶段,将被翻译成 、 等......例如,这是 names.c 中一个原始函数表的摘录:substitute(3 -> y)y <- 33 -> ydo_substituteSEXPEXPRSXP3 -> yy <- 3.ylhs -> rhsrhs <- lhs**^

/* 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 1/5/2016
与此同时,我敢说这个答案值得吗?好吧,我应该认真地回去工作了......gram.y
2赞 Josh O'Brien 1/5/2016
只是为了记录(也是作为一个完整的解析器新手),我会注意到令牌的类型(这里)和它的值(这里,分配给 )。在我看来,该类型用于指导表达式的解析(将我们发送到读取的分支),而它的值是通过第一个参数传递的(即)。RIGHT_ASSIGN<-yylvalinstall_and_save2{ $$ = xxbinary($2,$3,$1); setId( $$, @$); }xxbinary$2
0赞 nrussell 1/5/2016
@Josh O'Brien:感谢您的输入(以及编辑);从表面上看,这对我来说听起来很合理。如果你在某个时候愿意,请随时将它或任何其他相关信息添加到我的答案中(如果我试图自己表达,恐怕我会扼杀解释)。
3赞 Josh O'Brien 1/5/2016
@nrussell 不客气。 et al. 是内联函数,位于 $RHOME/src/include/Rinlinedfuns.h。在我看来,它们在这里的作用是将单个标记和解析的表达式组合成类似列表的语言对象,构建为输入表达式的完全解析版本。lang3
1赞 MichaelChirico 1/6/2016
感谢您的更新!至于,我确实记得至少在某个地方读到过那个运算符有点退化,所以至少我以前见过它被认为是一种被抛弃的人。无论如何,我构建的实用程序现在充满了可疑的有用知识......我是多么喜欢它!**