【问题标题】:How exactly does R parse `->`, the right-assignment operator?R 究竟如何解析 `->`,右赋值运算符?
【发布时间】:2016-04-08 12:44:06
【问题描述】:

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

?"-&gt;"?assignOpsR Language Definition 都只是顺便提及它作为正确的赋值运算符。

-&gt; 的使用方式显然有些独特之处。它不是一个函数/运算符(正如对getAnywhere 和直接对`-&gt;` 的调用似乎表明的那样),那么它是什么?它完全属于自己的一类吗?

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

【问题讨论】:

标签: r yacc


【解决方案1】:

让我先说我对解析器的工作原理一无所知。话虽如此,line 296 of gram.y 定义了以下标记来表示(YACC?)解析器 R 使用的赋值:

%token      LEFT_ASSIGN EQ_ASSIGN RIGHT_ASSIGN LBB

那么,on lines 5140 through 5150 of gram.c,这看起来像对应的 C 代码:

case '-':
  if (nextchar('>')) {
    if (nextchar('>')) {
      yylval = install_and_save2("<<-", "->>");
      return RIGHT_ASSIGN;
    }
    else {
      yylval = install_and_save2("<-", "->");
      return RIGHT_ASSIGN;
    }
  }

最后,从line 5044 of gram.c开始,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);
}

同样,在使用解析器的经验为零的情况下,-&gt;-&gt;&gt; 似乎分别以非常低的级别被直接翻译成 &lt;-&lt;&lt;-解释过程。


您在询问解析器如何“知道”将参数反转为-&gt; 时提出了一个很好的观点——考虑到-&gt; 似乎作为&lt;- 安装到R 符号表中——因此能够将x -&gt; y 正确解释为y &lt;- xnot x &lt;- y。我能做的最好的就是提供进一步的猜测,因为我继续遇到“证据”来支持我的主张。希望一些仁慈的 YACC 专家会偶然发现这个问题并提供一些见解;不过,我不会屏住呼吸。

回到lines 383 and 384 of gram.y,这看起来更像是一些与前面提到的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 (@987654352 @)。

这是我脑海中的想象:

LEFT_ASSIGN 场景:y &lt;- x

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

因此,生成的 (C?) 调用将是 xxbinary(&lt;-, y, x)

将此逻辑应用于RIGHT_ASSIGN,即x -&gt; y,结合我之前关于&lt;--&gt;被交换的猜想,

  • $2-&gt; 转换为 &lt;-
  • $1x
  • $3y

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


在此基础上更进一步,我们在line 3310 of gram.c 上定义了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 等)的正确定义,但我假设它用于评估特殊以与解释器同步的方式执行函数(即符号)。


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

1) 这真的是 R 中唯一表现出这种行为的对象吗? (我有 想起了约翰·钱伯斯在哈德利的书中引用的一句话:“一切 存在的是一个对象。发生的一切都是函数调用。” 这显然位于该域之外 - 还有其他类似的东西吗 这个?

首先,我同意这在该域之外。我相信钱伯斯的引用涉及 R 环境,即在这个低级解析阶段之后都发生的过程。但是,我将在下面进一步讨论这一点。无论如何,我能找到的此类行为的唯一其他示例是 ** 运算符,它是更常见的幂运算符 ^ 的同义词。与正确的分配一样,** 似乎没有被解释器“识别”为函数调用等......:

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

我发现这个是因为这是唯一一个 install_and_save2 is used by the C parser:

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...

当然我仍然在这里推测,但是是的,我认为我们可以有把握地假设,当您调用substitute(3 -&gt; y) 时,从the substitute function 的角度来看,表达式总是 @987654388 @;例如该函数完全不知道您输入了3 -&gt; ydo_substitute,就像 R 使用的 99% 的 C 函数一样,只处理 SEXP 参数 - 我相信在 3 -&gt; y 的情况下是 EXPRSXP(== y &lt;- 3)。这就是我在上面区分 R 环境和解析过程时所暗示的内容。我不认为有任何东西可以特别触发解析器启动 - 而是您输入到解释器中的 所有内容 都会被解析。昨晚我又一点阅读了有关 YACC / Bison 解析器 generator 的内容,据我了解(也就是不要把农场赌在这上面),Bison 使用您定义的语法(在.y 文件中)生成 C 中的解析器 - 即执行实际输入解析的 C 函数。反过来,您在 R 会话中输入的所有内容首先由这个 C 解析函数处理,然后委托在 R 环境中采取的适当操作(顺便说一下,我使用这个术语非常松散)。在此阶段,lhs -&gt; rhs 将被转换为 rhs &lt;- lhs**^ 等...例如,这是tables of primitive functions in 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}},

您会注意到这里没有定义-&gt;-&gt;&gt;**。据我所知,&lt;-[ 等 R 原始表达式是 R 环境与任何底层 C 代码最接近的交互。我的建议是,在这个阶段(从你在解释器中输入一组字符并点击“Enter”,一直到对有效 R 表达式的实际评估),解析器已经发挥了它的魔力,这就是为什么您无法像通常那样通过用反引号括起来来获得 -&gt;** 的函数定义。

【讨论】:

  • 同时我敢说这个答案值得gram.y吗?好的,我应该认真地回去工作...
  • 只是为了记录(也作为一个完整的解析器新手)我会注意到在令牌的类型(这里是RIGHT_ASSIGN)和它的值(这里是@987654410)之间似乎有a distinction @,由install_and_save2 分配给yylval)。在我看来,type 用于指导表达式的解析(将我们发送到读取 { $$ = xxbinary($2,$3,$1); setId( $$, @$); } 的分支),而它的 value 是通过传递的xxbinary 的第一个参数(即$2)。
  • @Josh O'Brien 感谢您的输入(以及编辑);从表面上看,这对我来说是合理的。如果您在某个时候愿意,请随时在我的答案中添加该信息或任何其他相关信息(如果我试图自己表达这一点,恐怕我会扼杀解释)。
  • @nrussell 不客气。 lang3 等。是内联函数,位于here, in $RHOME/src/include/Rinlinedfuns.h。在我看来,他们的角色是将各个标记和解析的表达式组合成类似列表的语言对象,构建输入表达式的完全解析版本。
  • 感谢更新!至于**,我确实记得至少在某个地方读到该操作员有点退化,所以至少我以前见过它被认为是一种弃儿。无论如何,我构建的实用程序现在充满了值得怀疑的有用知识......我多么喜欢它!
猜你喜欢
  • 2014-06-12
  • 2011-05-20
  • 1970-01-01
  • 2016-07-13
  • 2020-05-26
  • 2010-12-24
  • 1970-01-01
  • 1970-01-01
  • 2011-12-31
相关资源
最近更新 更多