21  Traducir código R

21.1 Introducción

La combinación de entornos de primera clase, alcance léxico y metaprogramación nos brinda un poderoso conjunto de herramientas para traducir código R a otros lenguajes. Un ejemplo completo de esta idea es dbplyr, que impulsa los backends de la base de datos para dplyr, lo que le permite expresar la manipulación de datos en R y traducirlos automáticamente a SQL. Puedes ver la idea clave en translate_sql() que toma el código R y devuelve el SQL equivalente:

library(dbplyr)

con <- simulate_dbi()

translate_sql(x ^ 2, con = con)
#> <SQL> POWER(`x`, 2.0)
translate_sql(x < 5 & !is.na(x), con = con)
#> <SQL> `x` < 5.0 AND NOT((`x` IS NULL))
translate_sql(!first %in% c("John", "Roger", "Robert"), con = con)
#> <SQL> NOT(`first` IN ('John', 'Roger', 'Robert'))
translate_sql(select == 7, con = con)
#> <SQL> `select` = 7.0

Traducir R a SQL es complejo debido a las muchas idiosincrasias de los dialectos de SQL, por lo que aquí desarrollaré dos lenguajes específicos de dominio (DSL) simples pero útiles: uno para generar HTML y el otro para generar ecuaciones matemáticas en LaTeX.

Si está interesado en obtener más información sobre los idiomas específicos del dominio en general, le recomiendo Idiomas específicos del dominio (Fowler 2010). Analiza muchas opciones para crear un DSL y proporciona muchos ejemplos de diferentes idiomas.

Estructura

  • La Sección 21.2 crea un DSL para generar HTML, usando quasiquotation y purrr para generar una función para cada etiqueta HTML, luego ordena la evaluación para acceder fácilmente a ellos.

  • La Sección 21.3) transforma matemáticamente el código R en su equivalente LaTeX usando una combinación de evaluación ordenada y caminata de expresión.

Requisitos previos

Este capítulo reúne muchas técnicas discutidas en otras partes del libro. En particular, deberá comprender los entornos, las expresiones, la evaluación ordenada y un poco de programación funcional y S3. Usaremos rlang para herramientas de metaprogramación y purrr para programación funcional.

library(rlang)
library(purrr)

21.2 HTML

HTML (lenguaje de marcado de hipertexto) subyace en la mayor parte de la web. Es un caso especial de SGML (Lenguaje de marcado generalizado estándar), y es similar pero no idéntico a XML (Lenguaje de marcado extensible). HTML se ve así:

<body>
  <h1 id='first'>A heading</h1>
  <p>Some text &amp; <b>some bold text.</b></p>
  <img src='myimg.png' width='100' height='100' />
</body>

Incluso si nunca antes ha mirado HTML, aún puede ver que el componente clave de su estructura de codificación son las etiquetas, que se ven como <tag></tag> o <tag/>. Las etiquetas se pueden anidar dentro de otras etiquetas y entremezclarse con el texto. Hay más de 100 etiquetas HTML, pero en este capítulo nos centraremos en unas pocas:

  • <body> es la etiqueta de nivel superior que contiene todo el contenido.
  • <h1> define un encabezado de nivel superior.
  • <p> define un párrafo.
  • <b> texto envalentonado.
  • <img> incrusta una imagen.

Las etiquetas pueden tener atributos con nombre que se parecen a <tag nombre1='valor1' nombre2='valor2'></tag>. Dos de los atributos más importantes son id y class, que se utilizan junto con CSS (hojas de estilo en cascada) para controlar la apariencia visual de la página.

Etiquetas anuladas, como <img>, no tienen hijos y se escriben <img />, no <img></img>. Dado que no tienen contenido, los atributos son más importantes, y img tiene tres que se usan con casi todas las imágenes: src (donde vive la imagen), width y height.

Debido a que < y > tienen significados especiales en HTML, no puede escribirlos directamente. En su lugar, debe usar los escapes de HTML: &gt; y &lt;. Y dado que esos escapes usan &, si quieres un ampersand literal, tienes que escapar como &amp;.

21.2.1 Objetivo

Nuestro objetivo es facilitar la generación de HTML desde R. Para dar un ejemplo concreto, queremos generar el siguiente HTML:

<body>
  <h1 id='first'>A heading</h1>
  <p>Some text &amp; <b>some bold text.</b></p>
  <img src='myimg.png' width='100' height='100' />
</body>

Usando el siguiente código que coincida lo más posible con la estructura del HTML:

with_html(
  body(
    h1("Un encabezado", id = "first"),
    p("Un poco de textp &", b("un poco de texto en negrita.")),
    img(src = "myimg.png", width = 100, height = 100)
  )
)

Este DSL tiene las siguientes tres propiedades:

  • El anidamiento de llamadas a funciones coincide con el anidamiento de etiquetas.

  • Los argumentos sin nombre se convierten en el contenido de la etiqueta y los argumentos con nombre se convierten en sus atributos.

  • & y otros caracteres especiales se escapan automáticamente.

21.2.2 Escapar

Escapar es tan fundamental para la traducción que será nuestro primer tema. Hay dos desafíos relacionados:

  • En la entrada del usuario, necesitamos escapar automáticamente &, < y >.

  • Al mismo tiempo, debemos asegurarnos de que &, < y > que generamos no tengan doble escape (es decir, que no generemos accidentalmente &amp;amp;, &amp;lt ; y &amp;gt;).

La forma más sencilla de hacer esto es crear una clase S3 (Sección 13.3) que distinga entre texto normal (que necesita escape) y HTML (que no).

html <- function(x) structure(x, class = "advr_html")

print.advr_html <- function(x, ...) {
  out <- paste0("<HTML> ", x)
  cat(paste(strwrap(out), collapse = "\n"), "\n", sep = "")
}

Luego escribimos un escape genérico. Tiene dos métodos importantes:

  • escape.character() toma un vector de caracteres regular y devuelve un vector HTML con caracteres especiales (&, <, >) escapados.

  • escape.advr_html() deja solo el HTML escapado.

escape <- function(x) UseMethod("escape")

escape.character <- function(x) {
  x <- gsub("&", "&amp;", x)
  x <- gsub("<", "&lt;", x)
  x <- gsub(">", "&gt;", x)

  html(x)
}

escape.advr_html <- function(x) x

Ahora comprobamos que funciona

escape("This is some text.")
#> <HTML> This is some text.
escape("x > 1 & y < 2")
#> <HTML> x &gt; 1 &amp; y &lt; 2

# Doble escape no es un problema
escape(escape("This is some text. 1 > 2"))
#> <HTML> This is some text. 1 &gt; 2

# Y el texto que sabemos que es HTML no se escapa.
escape(html("<hr />"))
#> <HTML> <hr />

Convenientemente, esto también permite que un usuario opte por nuestro escape si sabe que el contenido ya está escapado.

21.2.3 Funciones básicas de etiquetas

A continuación, escribiremos una función de una etiqueta a mano, luego descubriremos cómo generalizarla para que podamos generar una función para cada etiqueta con código.

Comencemos con <p>. Las etiquetas HTML pueden tener tanto atributos (por ejemplo, id o clase) como elementos secundarios (como <b> o <i>). Necesitamos alguna forma de separarlos en la llamada a la función. Dado que los atributos tienen nombre y los hijos no, parece natural usar argumentos con nombre y sin nombre para ellos, respectivamente. Por ejemplo, una llamada a p() podría verse así:

p("Texo. ", b(i("some bold italic text")), class = "mypara")

Podríamos enumerar todos los atributos posibles de la etiqueta <p> en la definición de la función, pero eso es difícil porque hay muchos atributos y porque es posible usar [atributos personalizados] (http://html5doctor.com/html5- atributos-de-datos-personalizados/). En su lugar, usaremos ... y separaremos los componentes en función de si tienen nombre o no. Con esto en mente, creamos una función auxiliar que envuelve rlang::list2() (Sección 19.6) y devuelve los componentes con nombre y sin nombre por separado:

dots_partition <- function(...) {
  dots <- list2(...)
  
 if (is.null(names(dots))) {
  is_named <- rep(FALSE, length(dots))
} else {
  is_named <- names(dots) != ""
}
  
  list(
    named = dots[is_named],
    unnamed = dots[!is_named]
  )
}

str(dots_partition(a = 1, 2, b = 3, 4))
#> List of 2
#>  $ named  :List of 2
#>   ..$ a: num 1
#>   ..$ b: num 3
#>  $ unnamed:List of 2
#>   ..$ : num 2
#>   ..$ : num 4

Ahora podemos crear nuestra función p(). Note que hay una nueva función aquí: html_attributes(). Toma una lista con nombre y devuelve la especificación del atributo HTML como una cadena. Es un poco complicado (en parte, porque trata con algunas idiosincrasias de HTML que no he mencionado aquí), pero no es tan importante y no introduce nuevas ideas de programación, así que no lo discutiré en detalle. Puede encontrar la fuente en línea si desea resolverlo usted mismo.

source("dsl-html-attributes.r")
p <- function(...) {
  dots <- dots_partition(...)
  attribs <- html_attributes(dots$named)
  children <- map_chr(dots$unnamed, escape)

  html(paste0(
    "<p", attribs, ">",
    paste(children, collapse = ""),
    "</p>"
  ))
}

p("Some text")
#> <HTML> <p>Some text</p>
p("Some text", id = "myid")
#> <HTML> <p id='myid'>Some text</p>
p("Some text", class = "important", `data-value` = 10)
#> <HTML> <p class='important' data-value='10'>Some text</p>

21.2.4 Funciones de etiquetas

Es sencillo adaptar p() a otras etiquetas: solo necesitamos reemplazar "p" con el nombre de la etiqueta. Una forma elegante de hacerlo es crear una función con rlang::new_function() (Sección 19.7.4), utilizando la eliminación de comillas y paste0() para generar las etiquetas de inicio y finalización.

tag <- function(tag) {
  new_function(
    exprs(... = ),
    expr({
      dots <- dots_partition(...)
      attribs <- html_attributes(dots$named)
      children <- map_chr(dots$unnamed, escape)

      html(paste0(
        !!paste0("<", tag), attribs, ">",
        paste(children, collapse = ""),
        !!paste0("</", tag, ">")
      ))
    }),
    caller_env()
  )
}
tag("b")
#> function (...) 
#> {
#>     dots <- dots_partition(...)
#>     attribs <- html_attributes(dots$named)
#>     children <- map_chr(dots$unnamed, escape)
#>     html(paste0("<b", attribs, ">", paste(children, collapse = ""), 
#>         "</b>"))
#> }

Necesitamos la extraña sintaxis exprs(... = ) para generar el argumento vacío ... en la función de etiqueta. Consulte la Sección 18.6.2 para obtener más detalles.

Ahora podemos ejecutar nuestro ejemplo anterior:

p <- tag("p")
b <- tag("b")
i <- tag("i")
p("Some text. ", b(i("some bold italic text")), class = "mypara")
#> <HTML> <p class='mypara'>Some text. <b><i>some bold italic
#> text</i></b></p>

Antes de generar funciones para cada etiqueta HTML posible, necesitamos crear una variante que maneje etiquetas vacías. void_tag() es bastante similar a tag(), pero arroja un error si hay etiquetas secundarias, como lo capturan los puntos sin nombre. La etiqueta en sí también se ve un poco diferente.

void_tag <- function(tag) {
  new_function(
    exprs(... = ),
    expr({
      dots <- dots_partition(...)
      if (length(dots$unnamed) > 0) {
        abort(!!paste0("<", tag, "> must not have unnamed arguments"))
      }
      attribs <- html_attributes(dots$named)

      html(paste0(!!paste0("<", tag), attribs, " />"))
    }),
    caller_env()
  )
}

img <- void_tag("img")
img
#> function (...) 
#> {
#>     dots <- dots_partition(...)
#>     if (length(dots$unnamed) > 0) {
#>         abort("<img> must not have unnamed arguments")
#>     }
#>     attribs <- html_attributes(dots$named)
#>     html(paste0("<img", attribs, " />"))
#> }
img(src = "myimage.png", width = 100, height = 100)
#> <HTML> <img src='myimage.png' width='100' height='100' />

21.2.5 Procesando todas las etiquetas

A continuación, debemos generar estas funciones para cada etiqueta. Comenzaremos con una lista de todas las etiquetas HTML:

tags <- c("a", "abbr", "address", "article", "aside", "audio",
  "b","bdi", "bdo", "blockquote", "body", "button", "canvas",
  "caption","cite", "code", "colgroup", "data", "datalist",
  "dd", "del","details", "dfn", "div", "dl", "dt", "em",
  "eventsource","fieldset", "figcaption", "figure", "footer",
  "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header",
  "hgroup", "html", "i","iframe", "ins", "kbd", "label",
  "legend", "li", "mark", "map","menu", "meter", "nav",
  "noscript", "object", "ol", "optgroup", "option", "output",
  "p", "pre", "progress", "q", "ruby", "rp","rt", "s", "samp",
  "script", "section", "select", "small", "span", "strong",
  "style", "sub", "summary", "sup", "table", "tbody", "td",
  "textarea", "tfoot", "th", "thead", "time", "title", "tr",
  "u", "ul", "var", "video"
)

void_tags <- c("area", "base", "br", "col", "command", "embed",
  "hr", "img", "input", "keygen", "link", "meta", "param",
  "source", "track", "wbr"
)

Si observa esta lista detenidamente, verá que hay bastantes etiquetas que tienen el mismo nombre que las funciones base de R (body, col, q, source, sub, summary, tabla). Esto significa que no queremos que todas las funciones estén disponibles de forma predeterminada, ya sea en el entorno global o en un paquete. En su lugar, los pondremos en una lista (como en la Sección 10.5) y luego proporcionaremos una ayuda para que sea más fácil usarlos cuando se desee. Primero, hacemos una lista con nombre que contiene todas las funciones de etiqueta:

html_tags <- c(
  tags %>% set_names() %>% map(tag),
  void_tags %>% set_names() %>% map(void_tag)
)

Esto nos da una forma explícita (pero detallada) de crear HTML:

html_tags$p(
  "Some text. ",
  html_tags$b(html_tags$i("some bold italic text")),
  class = "mypara"
)
#> <HTML> <p class='mypara'>Some text. <b><i>some bold italic
#> text</i></b></p>

Entonces podemos terminar nuestro DSL HTML con una función que nos permita evaluar el código en el contexto de esa lista. Aquí abusamos ligeramente de la máscara de datos, pasándole una lista de funciones en lugar de un marco de datos. Este es un truco rápido para mezclar el entorno de ejecución del código con las funciones en html_tags.

with_html <- function(code) {
  code <- enquo(code)
  eval_tidy(code, html_tags)
}

Esto nos brinda una API sucinta que nos permite escribir HTML cuando lo necesitamos, pero no abarrota el espacio de nombres cuando no lo necesitamos.

with_html(
  body(
    h1("A heading", id = "first"),
    p("Some text &", b("some bold text.")),
    img(src = "myimg.png", width = 100, height = 100)
  )
)
#> <HTML> <body><h1 id='first'>A heading</h1><p>Some text &amp;<b>some
#> bold text.</b></p><img src='myimg.png' width='100' height='100'
#> /></body>

Si desea acceder a la función R anulada por una etiqueta HTML con el mismo nombre dentro de with_html(), puede usar la especificación package::function completa.

21.2.6 Ejercicios

  1. Las reglas de escape para las etiquetas <script> son diferentes porque contienen JavaScript, no HTML. En lugar de escapar los corchetes angulares o los símbolos de unión, debe escapar </script> para que la etiqueta no se cierre demasiado pronto. Por ejemplo, script("'</script>'"), no debería generar esto:

    <script>'</script>'</script>

    But

    <script>'<\/script>'</script>

    Adapte escape() para seguir estas reglas cuando un nuevo argumento script se establezca en TRUE.

  2. El uso de ... para todas las funciones tiene algunas desventajas importantes. No hay validación de entrada y habrá poca información en la documentación o autocompletar sobre cómo se usan en la función. Cree una nueva función que, cuando se le proporcione una lista con nombre de etiquetas y sus nombres de atributos (como se muestra a continuación), cree funciones de etiqueta con argumentos con nombre.

    list(
      a = c("href"),
      img = c("src", "width", "height")
    )

    Todas las etiquetas deben obtener los atributos class e id.

  3. Razona sobre el siguiente código que llama with_html() haciendo referencia a objetos del entorno. ¿Funcionará o fallará? ¿Por qué? Ejecute el código para verificar sus predicciones.

    greeting <- "Hello!"
    with_html(p(greeting))
    
    p <- function() "p"
    address <- "123 anywhere street"
    with_html(p(address))
  4. Actualmente, el HTML no se ve muy bonito y es difícil ver la estructura. ¿Cómo podrías adaptar tag() para sangrar y formatear? (Es posible que deba investigar un poco sobre las etiquetas en bloque y en línea).

21.3 LaTeX

El próximo DSL convertirá las expresiones R en sus equivalentes matemáticos de LaTeX. (Esto es un poco como ?plotmath, pero para texto en lugar de gráficos.) LaTeX es la lingua franca de los matemáticos y estadísticos: es común usar la notación LaTeX cada vez que desea expresar una ecuación en texto, como en un correo electrónico. Dado que muchos informes se producen con R y LaTeX, podría ser útil poder convertir automáticamente expresiones matemáticas de un idioma a otro.

Debido a que necesitamos convertir funciones y nombres, este DSL matemático será más complicado que el HTML DSL. También necesitaremos crear una conversión predeterminada, para que los símbolos que no conocemos obtengan una conversión estándar. Esto significa que ya no podemos usar solo la evaluación: también necesitamos recorrer el árbol de sintaxis abstracta (AST).

21.3.1 LaTeX matemáticas

Antes de comenzar, veamos rápidamente cómo se expresan las fórmulas en LaTeX. El estándar completo es muy complejo, pero afortunadamente está bien documentado, y los comandos más comunes tienen una estructura bastante simple:

  • La mayoría de las ecuaciones matemáticas simples se escriben de la misma manera que las escribirías en R: x * y, z ^ 5. Los subíndices se escriben usando _ (por ejemplo, x_1).

  • Los caracteres especiales comienzan con \: \pi = \(\pi\), \pm = \(\pm\), y así sucesivamente. Hay una gran cantidad de símbolos disponibles en LaTeX: la búsqueda en línea de “símbolos matemáticos de látex” arroja muchas listas. Incluso hay un servicio que buscará el símbolo que dibujes en el navegador.

  • Las funciones más complicadas se ven como \name{arg1}{arg2}. Por ejemplo, para escribir una fracción usarías \frac{a}{b}. Para escribir una raíz cuadrada, usarías \sqrt{a}.

  • Para agrupar elementos, use {}: es decir, x ^ a + b versus x ^ {a + b}.

  • En una buena composición tipográfica matemática, se hace una distinción entre variables y funciones. Pero sin información adicional, LaTeX no sabe si f(a * b) representa llamar a la función f con la entrada a * b, o si es una abreviatura de f * (a * b). Si f es una función, puede decirle a LaTeX que la escriba usando una fuente vertical con \textrm{f}(a * b). (El rm significa “romano”, lo contrario de la cursiva).

21.3.2 Meta

Nuestro objetivo es usar estas reglas para convertir automáticamente una expresión R a su representación LaTeX adecuada. Abordaremos esto en cuatro etapas:

  • Convertir símbolos conocidos: pi\pi

  • Dejar otros símbolos sin cambios: xx, yy

  • Convertir funciones conocidas a sus formas especiales: sqrt(frac(a, b))\sqrt{\frac{a}{b}}

  • Envuelve funciones desconocidas con \textrm: f(a)\textrm{f}(a)

Codificaremos esta traducción en la dirección opuesta a lo que hicimos con HTML DSL. Comenzaremos con la infraestructura, porque eso hace que sea fácil experimentar con nuestro DSL, y luego trabajaremos de regreso para generar el resultado deseado.

21.3.3 to_math()

Para comenzar, necesitamos una función contenedora que convierta las expresiones R en expresiones matemáticas LaTeX. Esto funcionará como to_html() capturando la expresión no evaluada y evaluándola en un entorno especial. Hay dos diferencias principales:

  • El entorno de evaluación ya no es constante, ya que tiene que variar según la entrada. Esto es necesario para manejar símbolos y funciones desconocidos.

  • Nunca evaluamos en el entorno de argumentos porque estamos traduciendo cada función a una expresión LaTeX. El usuario necesitará usar explícitamente !! para evaluar normalmente.

Esto nos da:

to_math <- function(x) {
  expr <- enexpr(x)
  out <- eval_bare(expr, latex_env(expr))

  latex(out)
}

latex <- function(x) structure(x, class = "advr_latex")
print.advr_latex <- function(x) {
  cat("<LATEX> ", x, "\n", sep = "")
}

A continuación, construiremos latex_env(), comenzando de manera simple y haciéndonos progresivamente más complejos.

21.3.4 Símbolos conocidos

Nuestro primer paso es crear un entorno que convierta los símbolos especiales de LaTeX utilizados para los caracteres griegos, por ejemplo, pi a \pi. Usaremos el truco de la Sección 20.4.3 para vincular el símbolo pi al valor "\pi".

greek <- c(
  "alpha", "theta", "tau", "beta", "vartheta", "pi", "upsilon",
  "gamma", "varpi", "phi", "delta", "kappa", "rho",
  "varphi", "epsilon", "lambda", "varrho", "chi", "varepsilon",
  "mu", "sigma", "psi", "zeta", "nu", "varsigma", "omega", "eta",
  "xi", "Gamma", "Lambda", "Sigma", "Psi", "Delta", "Xi",
  "Upsilon", "Omega", "Theta", "Pi", "Phi"
)
greek_list <- set_names(paste0("\\", greek), greek)
greek_env <- as_environment(greek_list)

Entonces podemos comprobarlo:

latex_env <- function(expr) {
  greek_env
}

to_math(pi)
#> <LATEX> \pi
to_math(beta)
#> <LATEX> \beta

¡Se ve bien hasta ahora!

21.3.5 Símbolos desconocidos

Si un símbolo no es griego, queremos dejarlo como está. Esto es complicado porque no sabemos de antemano qué símbolos se utilizarán y no podemos generarlos todos. En su lugar, usaremos el enfoque descrito en la Sección 18.5: recorrer el AST para encontrar todos los símbolos. Esto nos da all_names_rec() y el asistente all_names():

all_names_rec <- function(x) {
  switch_expr(x,
    constant = character(),
    symbol =   as.character(x),
    call =     flat_map_chr(as.list(x[-1]), all_names)
  )
}

all_names <- function(x) {
  unique(all_names_rec(x))
}

all_names(expr(x + y + f(a, b, c, 10)))
#> [1] "x" "y" "a" "b" "c"

Ahora queremos tomar esa lista de símbolos y convertirla en un entorno para que cada símbolo se asigne a su representación de cadena correspondiente (por ejemplo, eval(quote(x), env) produce "x"). Nuevamente usamos el patrón de convertir un vector de caracteres con nombre en una lista y luego convertir la lista en un entorno.

latex_env <- function(expr) {
  names <- all_names(expr)
  symbol_env <- as_environment(set_names(names))

  symbol_env
}

to_math(x)
#> <LATEX> x
to_math(longvariablename)
#> <LATEX> longvariablename
to_math(pi)
#> <LATEX> pi

Esto funciona, pero necesitamos combinarlo con el entorno de símbolos griegos. Dado que queremos dar preferencia al griego sobre los valores predeterminados (por ejemplo, to_math(pi) debe dar "\\pi", no "pi"), symbol_env debe ser el padre de greek_env. Para hacer eso, necesitamos hacer una copia de greek_env con un nuevo padre. Esto nos da una función que puede convertir símbolos conocidos (griegos) y desconocidos.

latex_env <- function(expr) {
  # Símbolos desconocidos
  names <- all_names(expr)
  symbol_env <- as_environment(set_names(names))

  # Símbolos conocidos
  env_clone(greek_env, parent = symbol_env)
}

to_math(x)
#> <LATEX> x
to_math(longvariablename)
#> <LATEX> longvariablename
to_math(pi)
#> <LATEX> \pi

21.3.6 Funciones conocidas

A continuación agregaremos funciones a nuestro DSL. Comenzaremos con un par de ayudantes que facilitan la adición de nuevos operadores binarios y unarios. Estas funciones son muy simples: solo ensamblan cadenas.

unary_op <- function(left, right) {
  new_function(
    exprs(e1 = ),
    expr(
      paste0(!!left, e1, !!right)
    ),
    caller_env()
  )
}

binary_op <- function(sep) {
  new_function(
    exprs(e1 = , e2 = ),
    expr(
      paste0(e1, !!sep, e2)
    ),
    caller_env()
  )
}

unary_op("\\sqrt{", "}")
#> function (e1) 
#> paste0("\\sqrt{", e1, "}")
binary_op("+")
#> function (e1, e2) 
#> paste0(e1, "+", e2)

Usando estos ayudantes, podemos mapear algunos ejemplos ilustrativos de conversión de R a LaTeX. Tenga en cuenta que con las reglas de alcance léxico de R ayudándonos, podemos proporcionar fácilmente nuevos significados para funciones estándar como +, - y *, e incluso ( y {.

# Operadores binarios
f_env <- child_env(
  .parent = empty_env(),
  `+` = binary_op(" + "),
  `-` = binary_op(" - "),
  `*` = binary_op(" * "),
  `/` = binary_op(" / "),
  `^` = binary_op("^"),
  `[` = binary_op("_"),

  # Agrupamiento
  `{` = unary_op("\\left{ ", " \\right}"),
  `(` = unary_op("\\left( ", " \\right)"),
  paste = paste,

  # Otras funciones matemáticas
  sqrt = unary_op("\\sqrt{", "}"),
  sin =  unary_op("\\sin(", ")"),
  log =  unary_op("\\log(", ")"),
  abs =  unary_op("\\left| ", "\\right| "),
  frac = function(a, b) {
    paste0("\\frac{", a, "}{", b, "}")
  },

  # Etiquetado
  hat =   unary_op("\\hat{", "}"),
  tilde = unary_op("\\tilde{", "}")
)

Nuevamente modificamos latex_env() para incluir este entorno. Debería ser el último entorno en el que R busca nombres para que expresiones como sin(sin) funcionen.

latex_env <- function(expr) {
  # Funciones conocidas
  f_env

  # Símbolos predeterminados
  names <- all_names(expr)
  symbol_env <- as_environment(set_names(names), parent = f_env)

  # Símbolos conocidos
  greek_env <- env_clone(greek_env, parent = symbol_env)

  greek_env
}

to_math(sin(x + pi))
#> <LATEX> \sin(x + \pi)
to_math(log(x[i]^2))
#> <LATEX> \log(x_i^2)
to_math(sin(sin))
#> <LATEX> \sin(sin)

21.3.7 Funciones desconocidas

Finalmente, agregaremos un valor predeterminado para las funciones que aún no conocemos. No podemos saber de antemano cuáles serán las funciones desconocidas, por lo que nuevamente recorremos el AST para encontrarlas:

all_calls_rec <- function(x) {
  switch_expr(x,
    constant = ,
    symbol =   character(),
    call = {
      fname <- as.character(x[[1]])
      children <- flat_map_chr(as.list(x[-1]), all_calls)
      c(fname, children)
    }
  )
}
all_calls <- function(x) {
  unique(all_calls_rec(x))
}

all_calls(expr(f(g + b, c, d(a))))
#> [1] "f" "+" "d"

Necesitamos un cierre que genere las funciones para cada llamada desconocida:

unknown_op <- function(op) {
  new_function(
    exprs(... = ),
    expr({
      contents <- paste(..., collapse = ", ")
      paste0(!!paste0("\\mathrm{", op, "}("), contents, ")")
    })
  )
}
unknown_op("foo")
#> function (...) 
#> {
#>     contents <- paste(..., collapse = ", ")
#>     paste0("\\mathrm{foo}(", contents, ")")
#> }
#> <environment: 0x5562aa2262c8>

Y de nuevo la actualizamos latex_env():

latex_env <- function(expr) {
  calls <- all_calls(expr)
  call_list <- map(set_names(calls), unknown_op)
  call_env <- as_environment(call_list)

  # Funciones conocidas
  f_env <- env_clone(f_env, call_env)

  # Símbolos predeterminados
  names <- all_names(expr)
  symbol_env <- as_environment(set_names(names), parent = f_env)

  # Símbolos conocidos
  greek_env <- env_clone(greek_env, parent = symbol_env)
  greek_env
}

Esto completa nuestros requisitos originales:

to_math(sin(pi) + f(a))
#> <LATEX> \sin(\pi) + \mathrm{f}(a)

Sin duda, podría llevar esta idea más allá y traducir tipos de expresiones matemáticas, pero no debería necesitar ninguna herramienta de metaprogramación adicional.

21.3.8 Ejercicios

  1. Añadir escape. Los símbolos especiales que deben escaparse agregando una barra invertida delante de ellos son \, $ y %. Al igual que con HTML, deberá asegurarse de no terminar con doble escape. Por lo tanto, deberá crear una clase S3 pequeña y luego usarla en los operadores de funciones. Eso también le permitirá incrustar LaTeX arbitrario si es necesario.

  2. Complete el DSL para admitir todas las funciones que admite plotmath.