library(dbplyr)
<- simulate_postgres()
con
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
21 Translating R code
Introduction
first-class环境,词法作用域,元编程组成了一套实现将R代码转换为其他语言的工具箱。例如,dbplyr
为dplyr
处理数据库提供了支持,允许用R语言表达数据操作并自动将其翻译成SQL,可以使用translate_sql()
一览其关键思想:
由于SQL语言有许多特性,将R语言翻译成SQL语言的机制非常复杂,因此本章我们介绍两种简单但有用的领域特定语言(DSL): 一种用于生成HTML, 另一种用于在LaTeX中生成数学方程。
Outline
21.2节:介绍创建HTML。
21.2节:介绍创建LaTeX。
Prerequisites
学习本章,你需要了解:环境、表达式、整洁评估、泛函编程、元编程、S3面向对象等。
library(rlang)
library(purrr)
#>
#> Attaching package: 'purrr'
#> The following objects are masked from 'package:rlang':
#>
#> %@%, flatten, flatten_chr, flatten_dbl, flatten_int,
#> flatten_lgl, flatten_raw, invoke, splice
HTML
HTML文件是网站的底层核心,是一种特殊的标记语言(SGML,Standard Generalised Markup Language),它和XML相似但不等同。
<body>
<h1 id='first'>A heading</h1>
<p>Some text & <b>some bold text.</b></p>
<img src='myimg.png' width='100' height='100' />
</body>
HTML文件中的关键组件是标签(tag),形如<tag></tag>
或<tag/>
。标签可以嵌套在其他标签中,并与文本交织在一起。HTML标签有超过100个,但在本章中,我们只关注其中的少数几个:
<body>
:文档的主体,包含文档所有内容的顶级标签。<h1>
:文档的标题级别。<p>
:段落。<b>
:粗体。<img>
:图片。
标签具有带名字的属性,形如<tag name1='value1' name2='value2'/></tag>
。其中有两个重要的属性——id
和class
,它们会与CSS联合使用来控制页面的外观。
<img>
标签不包裹任何内容,它只能被写作<img />
而不能写成<img></img>
,类似<img>
的标签被称为空标签(Void tags)。因为它们不能包裹内容,所以它们的属性非常重要,<img>
有三个常被使用的属性:scr
控制图片路径,width
和height
控制图片大小。
因为<
和>
是HTML中的特殊字符,想要在文本中书写它们,必须用转义符<
和>
来代替。同样,&
也必须用转义符&
来代替。
Goal
我们的目标是使用R生成上面的模板html文档。类似:
with_html(
body(
h1("A heading", id = "first"),
p("Some text &", b("some bold text.")),
img(src = "myimg.png", width = 100, height = 100)
) )
它有三个特点:
- 函数名与标签名相同。
- 未命名参数成为标签的内容,而命名参数成为其属性。
- & 和其他特殊字符会自动转义。
Escaping
转义功能对于代码“翻译”至关重要,它有两个难点:
对输入的字符进行自动转义,
&
,<
,>
。正确识别是否需要转义,防止
&
变为&amp;
。
解决这两个难点的最简单方法是使用S3面向对象,区分要进行转义的普通字符,和已经转义的字符(类)。
<- function(x) structure(x, class = "advr_html")
html
<- function(x, ...) {
print.advr_html <- paste0("<HTML> ", x)
out cat(paste(strwrap(out), collapse = "\n"), "\n", sep = "")
}
创建转义泛函和它针对两种类的方法:
escape.character()
:对普通字符进行转义。escape.advr_html()
:对已经转义的字符不做任何处理。
<- function(x) UseMethod("escape")
escape
<- function(x) {
escape.character <- gsub("&", "&", x)
x <- gsub("<", "<", x)
x <- gsub(">", ">", x)
x
html(x)
}
<- function(x) x escape.advr_html
检查它是否运行成功:
escape("This is some text.")
#> <HTML> This is some text.
escape("x > 1 & y < 2")
#> <HTML> x > 1 & y < 2
# Double escaping is not a problem
escape(escape("This is some text. 1 > 2"))
#> <HTML> This is some text. 1 > 2
# And text we know is HTML doesn't get escaped.
escape(html("<hr />"))
#> <HTML> <hr />
Basic tag functions
接下来,我们将手动编写一个单标签函数,然后弄清楚如何对其进行泛化,这样我们就可以用代码为每个标签生成一个函数。
我们以<p>
标签为例。HTML的标签可以同时具有属性和子标签(<b>
,<i>
)。考虑到属性有name,子标签没有,我们可以将它们类比为函数参数,在函数内部处理两种类型的参数。p()
函数的使用方法可能会类似于:
p("Some text. ", b(i("some bold italic text")), class = "mypara")
考虑到标签拥有的属性数目不同,子标签的数量也会不同。我们需要使用...
来获取参数,然后根据是否有name属性进行分类。
<- function(...) {
dots_partition <- list2(...)
dots
if (is.null(names(dots))) {
<- rep(FALSE, length(dots))
is_named else {
} <- names(dots) != ""
is_named
}
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
现在我们可以创建p()
函数了。示例中引入了一些新的函数,这里不再详细讨论。
<- function(list) {
html_attributes if (length(list) == 0) {
return("")
}
<- map2_chr(names(list), list, html_attribute)
attr paste0(" ", unlist(attr), collapse = "")
}<- function(name, value = NULL) {
html_attribute if (length(value) == 0) {
return(name)
# for attributes with no value
} if (length(value) != 1) stop("`value` must be NULL or length 1")
if (is.logical(value)) {
# Convert T and F to true and false
<- tolower(value)
value else {
} <- escape_attr(value)
value
}paste0(name, "='", value, "'")
}<- function(x) {
escape_attr <- escape.character(x)
x <- gsub("\'", "'", x)
x <- gsub("\"", """, x)
x <- gsub("\r", " ", x)
x <- gsub("\n", " ", x)
x
x
}
<- function(...) {
p <- dots_partition(...)
dots <- html_attributes(dots$named)
attribs <- map_chr(dots$unnamed, escape)
children
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>
Tag functions
创建其他的标签函数,我们只需要替换p
即可。所以tag()
接受一个标签参数,返回一个rlang::new_function()
创建的函数。new_function()
内使用exprs(... = )
来捕获参数。
<- function(tag) {
tag new_function(
exprs(... = ),
expr({
<- dots_partition(...)
dots <- html_attributes(dots$named)
attribs <- map_chr(dots$unnamed, escape)
children
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>"))
#> }
现在可以复现上面的函数样式了:
<- tag("p")
p <- tag("b")
b <- tag("i")
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>
在创建所有HTML标签函数前,需要为空标签类型创建泛函void_tag()
。它与tag()
函数类似,但在出现子标签时报错。
<- function(tag) {
void_tag new_function(
exprs(... = ),
expr({
<- dots_partition(...)
dots if (length(dots$unnamed) > 0) {
abort(!!paste0("<", tag, "> must not have unnamed arguments"))
}<- html_attributes(dots$named)
attribs
html(paste0(!!paste0("<", tag), attribs, " />"))
}),caller_env()
)
}
<- void_tag("img")
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' />
LaTeX
使用R语言生成LaTeX语句会麻烦一些,因为要同时处理函数名和参数的转换。这意味着,我们需要使用抽象语法树(AST)来修改代码。
LaTeX mathematics
在开始之前,先简单介绍一下LaTeX中公式的表达方式。完整的标准非常复杂,但幸运的是,相关文档非常详细,而且最常见的命令结构相当简单:
大多数简单的数学方程写法与在R中输入它们的方式相同:
x * y
,z ^ 5
。下标使用_
(例如,x_1
)。特殊符号使用
\
转义:\pi
= ,pm
= 。LaTeX中有大量这种符号,可以在网上搜索,或者使用http://detexify.kirelabs.org/classify.html。复杂函数,形如
\name{arg1}{arg2}
。例如,分数\frac{a}{b}
,开方\sqrt{a}
。使用
{}
将元素分组:x ^ a + b
与x ^ {a + b}
。区分函数与变量。使用
\textrm{f}(a * b)
来标识f
是函数,a * b
是变量,不然无法确定f
是函数还是变量。
Goal
我们的目标是使用这些规则自动将R表达式转换为适当的LaTeX表示形式。我们将分四个阶段处理这个问题:
转换已知的符号:
pi
->\pi
保留其他符号不变:
x
->x
,y
->y
转换已知的函数为特殊符号:
sqrt(frac(a,b))
->\sqrt{\frac{a}{b}}
使用
\textrm{}
标识其他函数:f(a)
->\textrm{f}(a)
to_math()
首先,我们封装一个函数,将R表达式转换为LaTeX数学表达式。这将类似于to_html()
,通过捕获未计算的表达式并在特殊环境中对其进行计算来实现。主要有两个区别:
评估环境不再是恒定的,因为它必须根据输入而变化。这对于处理未知符号和函数是必要的。
我们从不在参数环境中计算,因为我们将每个函数都转换为LaTeX表达式。用户需要使用
!!
才能正常计算。
<- function(x) {
to_math <- enexpr(x)
expr <- eval_bare(expr, latex_env(expr))
out
latex(out)
}
<- function(x) structure(x, class = "advr_latex")
latex <- function(x) {
print.advr_latex cat("<LATEX> ", x, "\n", sep = "")
}
我们会逐步构建latex_env()
。
Known symbols
第一步,创建一个能生成在LaTeX中用来表示希腊字符的特殊字符的环境。如,pi
转换为\pi
。
<- c(
greek "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"
)<- set_names(paste0("\\", greek), greek)
greek_list <- as_environment(greek_list)
greek_env
<- function(expr) {
latex_env
greek_env
}
to_math(pi)
#> <LATEX> \pi
to_math(beta)
#> <LATEX> \beta
Unknown symbols
第二步,保留不是希腊字符的符号为原样。但有个问题是:我们无法预先知道输入的符号是什么,没法创建类似greek_env
的环境。幸运的是,我们可以使用抽象语法树提取“表达式”中的字符。
<- function(x) {
expr_type if (rlang::is_syntactic_literal(x)) {
"constant"
else if (is.symbol(x)) {
} "symbol"
else if (is.call(x)) {
} "call"
else if (is.pairlist(x)) {
} "pairlist"
else {
} typeof(x)
}
}
<- function(x, ...) {
switch_expr switch(expr_type(x),
...,stop("Don't know how to handle type ", typeof(x), call. = FALSE)
)
}
<- function(.x, .f, ...) {
flat_map_chr ::flatten_chr(purrr::map(.x, .f, ...))
purrr
}
<- function(x) {
all_names_rec switch_expr(
x,constant = character(),
symbol = as.character(x),
call = flat_map_chr(as.list(x[-1]), all_names)
)
}
<- function(x) {
all_names unique(all_names_rec(x))
}
all_names(expr(x + y + f(a, b, c, 10)))
#> [1] "x" "y" "a" "b" "c"
现在,我们可以从输入的“表达式”中提取所有符号并创建环境。
<- function(expr) {
latex_env <- all_names(expr)
names <- as_environment(set_names(names))
symbol_env
symbol_env
}
to_math(x)
#> <LATEX> x
to_math(longvariablename)
#> <LATEX> longvariablename
to_math(pi)
#> <LATEX> pi
接下来,我们需要将两个环境结合,将symbol_env
设置为greek_env
的父环境。
<- function(expr) {
latex_env # Unknown symbols
<- all_names(expr)
names <- as_environment(set_names(names))
symbol_env
# Known symbols
env_clone(greek_env, parent = symbol_env)
}
to_math(x)
#> <LATEX> x
to_math(longvariablename)
#> <LATEX> longvariablename
to_math(pi)
#> <LATEX> \pi
Known functions
第三步,添加函数。
首先,我们将介绍一些辅助函数,它们可以轻松地添加新的一元和二元运算符。这些函数非常简单:它们只是组合字符串。
<- function(left, right) {
unary_op new_function(
exprs(e1 = ),
expr(
paste0(!!left, e1, !!right)
),caller_env()
)
}
<- function(sep) {
binary_op 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)
使用这些辅助函数,我们可以映射一些将R转换为LaTeX的示例。请注意,有了R的词法作用域规则的帮助,我们可以轻松地为标准函数如+
、-
和*
, 甚至(
和{
提供新的含义。
# Binary operators
<- child_env(
f_env .parent = empty_env(),
`+` = binary_op(" + "),
`-` = binary_op(" - "),
`*` = binary_op(" * "),
`/` = binary_op(" / "),
`^` = binary_op("^"),
`[` = binary_op("_"),
# Grouping
`{` = unary_op("\\left{ ", " \\right}"),
`(` = unary_op("\\left( ", " \\right)"),
paste = paste,
# Other math functions
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, "}")
},
# Labelling
hat = unary_op("\\hat{", "}"),
tilde = unary_op("\\tilde{", "}")
)
我们再次修改latex_env()
以包含这个环境。这应该是R查找名称的最后一个环境,这样像sin(sin)
这样的表达式才能工作。
<- function(expr) {
latex_env # Known functions
f_env
# Default symbols
<- all_names(expr)
names <- as_environment(set_names(names), parent = f_env)
symbol_env
# Known symbols
<- env_clone(greek_env, parent = symbol_env)
greek_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)
Unknown functions
第四步,添加未知函数到环境中。同样,我们再次使用抽象语法树来提取:
<- function(x) {
all_calls_rec switch_expr(x, constant = , symbol = character(), call = {
<- as.character(x[[1]])
fname <- flat_map_chr(as.list(x[-1]), all_calls)
children c(fname, children)
})
}<- function(x) {
all_calls unique(all_calls_rec(x))
}
all_calls(expr(f(g + b, c, d(a))))
#> [1] "f" "+" "d"
创建一个闭包函数生成未知函数:
<- function(op) {
unknown_op new_function(
exprs(... = ),
expr({
<- paste(..., collapse = ", ")
contents paste0(!!paste0("\\mathrm{", op, "}("), contents, ")")
})
)
}unknown_op("foo")
#> function (...)
#> {
#> contents <- paste(..., collapse = ", ")
#> paste0("\\mathrm{foo}(", contents, ")")
#> }
#> <environment: 0x0000020234685060>
更新latex_env()
:
<- function(expr) {
latex_env <- all_calls(expr)
calls <- map(set_names(calls), unknown_op)
call_list <- as_environment(call_list)
call_env
# Known functions
<- env_clone(f_env, call_env)
f_env
# Default symbols
<- all_names(expr)
names <- as_environment(set_names(names), parent = f_env)
symbol_env
# Known symbols
<- env_clone(greek_env, parent = symbol_env)
greek_env
greek_env }
测试:
to_math(sin(pi) + f(a))
#> <LATEX> \sin(\pi) + \mathrm{f}(a)
你可以进一步拓展这个想法,翻译数学表达式的类型,但你应该不再需要任何额外的元编程工具了。