usar - ¿Cómo exactamente R analiza `->`, el operador de asignación correcta?
que es bison (1)
Entonces, esta es una pregunta trivial, pero me molesta que no pueda responderla, y tal vez la respuesta me enseñe algunos detalles más sobre cómo funciona R.
El título lo dice todo: ¿cómo analiza R
->
, la oscura función de asignación del lado derecho?
Mis trucos habituales para sumergirme en esto fallaron:
`->`
Error: objeto
->
no encontrado
getAnywhere("->")
no se encontró ningún objeto llamado
->
Y no podemos llamarlo directamente:
`->`(3,x)
Error: no se pudo encontrar la función
"->"
Pero, por supuesto, funciona:
(3 -> x) #assigns the value 3 to the name x
# [1] 3
Parece que R sabe cómo simplemente revertir los argumentos, pero pensé que los enfoques anteriores seguramente habrían resuelto el caso:
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)
Compare esto con el operador de asignación regular:
`<-`
.Primitive("<-")
`<-`(x, 3) #assigns the value 3 to the name x, as expected
?"->"
?assignOps
, y la
Definición del lenguaje R
simplemente lo mencionan de pasada como el operador de asignación correcto.
Pero claramente hay algo único acerca de cómo
->
se usa.
No es una función / operador (como parecen demostrar las llamadas a
getAnywhere
y directamente a
`->`
), entonces, ¿qué es?
¿Está completamente en una clase propia?
¿Hay algo que aprender de esto además de "
->
es completamente único dentro del lenguaje R en cómo se interpreta y maneja; memorizar y seguir adelante"?
Permítanme presentar esto diciendo que no sé absolutamente nada sobre cómo funcionan los analizadores sintácticos. Dicho esto, la línea 296 de gram.y define los siguientes tokens para representar la asignación en los usos del analizador R (¿YACC?):
%token LEFT_ASSIGN EQ_ASSIGN RIGHT_ASSIGN LBB
Luego, en las líneas 5140 a 5150 de gram.c , esto se parece al código C correspondiente:
case ''-'':
if (nextchar(''>'')) {
if (nextchar(''>'')) {
yylval = install_and_save2("<<-", "->>");
return RIGHT_ASSIGN;
}
else {
yylval = install_and_save2("<-", "->");
return RIGHT_ASSIGN;
}
}
Finalmente, comenzando en la
línea 5044 de gram.c
, la definición de
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);
}
De nuevo, al tener cero experiencia trabajando con analizadores sintéticos, parece que
->
y
->>
se traducen directamente en
<-
y
<<-
, respectivamente, a un
nivel muy bajo
en el proceso de interpretación.
Trajo un muy buen punto al preguntar cómo el analizador "sabe" invertir los argumentos a
->
, considerando que
->
parece estar instalado en la tabla de símbolos R como
<-
- y así poder interpretar correctamente
x -> y
como
y <- x
y
no
x <- y
.
Lo mejor que puedo hacer es proporcionar más especulaciones a medida que continúo encontrando "evidencia" para respaldar mis afirmaciones.
Con suerte, algún experto misericordioso de YACC tropezará con esta pregunta y proporcionará una pequeña idea;
Sin embargo, no voy a contener la respiración.
Volviendo a las
líneas 383 y 384 de gram.y
, esto parece una lógica de análisis más relacionada con los símbolos
LEFT_ASSIGN
y
RIGHT_ASSIGN
mencionados
LEFT_ASSIGN
:
| expr LEFT_ASSIGN expr { $$ = xxbinary($2,$1,$3); setId( $$, @$); }
| expr RIGHT_ASSIGN expr { $$ = xxbinary($2,$3,$1); setId( $$, @$); }
Aunque realmente no puedo hacer cara o cruz de esta sintaxis loca, noté que el segundo y el tercer argumento de
xxbinary
se
xxbinary
a WRT
LEFT_ASSIGN
(
xxbinary($2,$1,$3)
) y
RIGHT_ASSIGN
(
xxbinary($2,$3,$1)
).
Esto es lo que estoy imaginando en mi cabeza:
LEFT_ASSIGN
Escenario:
y <- x
-
$2
es el segundo "argumento" para el analizador en la expresión anterior, es decir,<-
-
$1
es el primero; a saber,y
-
$3
es el tercero;x
Por lo tanto, la llamada resultante (C?) Sería
xxbinary(<-, y, x)
.
Aplicando esta lógica a
RIGHT_ASSIGN
, es decir,
x -> y
, combinado con mi conjetura anterior sobre
<-
y
->
intercambiando,
-
$2
se traducen de->
a<-
-
$1
esx
-
$3
esy
Pero como el resultado es
xxbinary($2,$3,$1)
lugar de
xxbinary($2,$1,$3)
, el resultado sigue
siendo
xxbinary(<-, y, x)
.
Partiendo de esto un poco más, tenemos la definición de
xxbinary
en la
línea 3310 de gram.c
:
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;
}
Desafortunadamente, no pude encontrar una definición adecuada de
lang3
(o sus variantes
lang1
,
lang2
, etc.) en el código fuente R, pero supongo que se usa para evaluar funciones especiales (es decir, símbolos) de una manera que está sincronizado con el intérprete.
Actualizaciones Intentaré abordar algunas de sus preguntas adicionales en los comentarios lo mejor que pueda, dado mi (muy) limitado conocimiento del proceso de análisis.
1) ¿Es realmente el único objeto en R que se comporta así? (Tengo en mente la cita de John Chambers a través del libro de Hadley: "Todo lo que existe es un objeto. Todo lo que sucede es una llamada a la función". Esto claramente se encuentra fuera de ese dominio, ¿hay algo más como esto?
Primero, estoy de acuerdo en que esto se encuentra fuera de ese dominio.
Creo que la cita de Chambers se refiere al entorno R, es decir, procesos que tienen lugar después de esta fase de análisis de bajo nivel.
Sin embargo, tocaré esto un poco más abajo.
De todos modos, el único otro ejemplo de este tipo de comportamiento que pude encontrar es el operador
**
, que es sinónimo del operador de exponenciación más común
^
.
Al igual que con la asignación correcta,
**
no parece ser "reconocido" como una llamada de función, etc., por el intérprete:
R> `->`
#Error: object ''->'' not found
R> `**`
#Error: object ''**'' not found
Encontré esto porque es el único otro caso donde
install_and_save2
es usado por el analizador C
:
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) ¿Cuándo sucede exactamente esto? Tengo en cuenta que el sustituto (3 -> y) ya ha cambiado la expresión; No pude deducir de la fuente qué sustituto hace que hubiera afectado al YACC ...
Por supuesto, todavía estoy especulando aquí, pero sí, creo que podemos asumir con seguridad que cuando llamas a
substitute(3 -> y)
, desde la perspectiva de
la función sustituto
, la expresión
siempre fue
y <- 3
;
por ejemplo, la función ignora por completo que usted escribió
3 -> y
.
do_substitute
, como el 99% de las funciones de C utilizadas por R, solo maneja argumentos
SEXP
, un
EXPRSXP
en el caso de
3 -> y
(==
y <- 3
), creo.
Esto es a lo que me refería anteriormente cuando hice una distinción entre el R Environment y el proceso de análisis.
No creo que haya nada que active específicamente el analizador para que entre en acción, sino que
todo lo
que ingresas en el intérprete se analiza.
Leí un
poco
más sobre el
generador de
analizador YACC / Bison anoche, y como lo entiendo (también conocido como no apostar la granja en esto), Bison usa la gramática que usted define (en el (los) archivo (s)
.y
)
generar
un analizador en C, es decir, una función C que realiza el análisis real de la entrada.
A su vez, todo lo que ingresa en una sesión R se procesa primero por esta función de análisis C, que luego delega la acción apropiada que se tomará en el Entorno R (por cierto, estoy usando este término de manera muy flexible).
Durante esta fase,
lhs -> rhs
se traducirá a
rhs <- lhs
,
**
a
^
, etc ... Por ejemplo, este es un extracto de una de las
tablas de funciones primitivas en nombres.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}},
Notará que
->
,
->>
y
**
no están definidos aquí.
Hasta donde yo sé, las expresiones primitivas R como
<-
y
[
, etc ... son la interacción más cercana que R Environment tiene con cualquier código C subyacente.
Lo que sugiero es que en esta etapa del proceso (desde que escribes un conjunto de caracteres en el intérprete y presionas ''Enter'', hasta la evaluación real de una expresión R válida), el analizador ya ha trabajado su magia, por eso no puede obtener una definición de función para
->
o
**
al rodearlos con backticks, como normalmente puede hacerlo.