R 4.1.0 的新特性 & 使用 hotkeys 配置输入快捷键

原文链接:https://www.jumpingrivers.com/blog/new-features-r410-pipe-anonymous-functions/

R 4.1.0 已发布!

R的新版本(v 4.1.0)已于 2021 年 5 月 18 日发布。通常来讲,大部分 R 更新不会包含太多的新特性,但这次更新确实有一些有趣且重要的变化。

本推文总结了一些值得注意的变化,其他具体更新内容请移步 R 更新日志。

1. 对 32 位 Windows 系统支持减少

R 4.1.x 系列是最后一批支持 32 位 Windows 的 R 版本。虽然这对大多数人没有影响,但如果你有一台旧笔记本,偶尔用来玩玩R,就可能会带来问题。

2. 新的语法

基础 R 包的稳定性是 R 生态的一个重要优势:基础包既支持了扩展包(CRAN, BioConductor),也与之形成了鲜明对比。

可以想象一下在R中引入一些新特性的情形,即使这些特性的可用性出现问题,也会至少需要维护到下一个重大版本更新,届时可能会有成千上万的开发人员和分析师会开始使用这些特性。因此,R 维护人员在引入新语法时会非常谨慎。

同样,在你自己的代码中使用新语法时也应该谨慎。如果你使用了 R 4.1.x 中引入的新语法,你要注意自己的代码可能无法在之前的R版本中运行。例如你写的数据分析代码可能在你同事的电脑上就无法正常工作,或者其他用户无法安装你开发的新R包。

2.1 原生管道符

magrittr 包在2014年将管道符引入R。它建立在 Unix/Linux 脚本语言(bash)和函数式编程语言(如 F#)中类似的语法上,旨在:

减少开发时间,提高代码的可读性和可维护性。Bache, 2014.

为了体现管道语法的价值,我们先给大家看一段调用嵌套函数的代码:

## 绘制抽样分布的密度图:
plot(
density(
rnorm(100, mean = 4, sd = 1)
)
)
示例图

你可能想分步骤来写,以便容易地确定每个函数的调用顺序(先是调用rnorm(),然后是density(),最后是plot()):

## 绘制抽样分布的密度图:
raw_data <- rnorm(100, mean = 4, sd = 1)
density_summary <- density(raw_data)
plot(density_summary)
示例图

但除非你会再次使用抽样结果 raw_data 或者抽样结果的分布 density_summary,否则这样写代码就会向你的R环境中引入许多冗余变量。magrittr 包提供了一个非常好的折中方案,既减少了冗余变量,也体现了代码中函数调用的自然顺序:

## R 4.0.5
library(magrittr)
rnorm(100, mean = 4, sd = 1) %>%
density() %>%
plot()
示例图

R4.1.0基础语法中加入了原生管道符|>

## R 4.1.0
rnorm(100, mean = 4, sd = 1) |>
density() |>
plot()
示例图

新管道符比 magrittr 管道符更容易输入(在两者都没有设置快捷键的情况下),更高效,也更容易 debug。

2.2 原生管道符 VS. magrittr

注意,|> 并非对%>%的完全替代。使用 magrittr 管道符时,函数可以带括号,也可以不带:

## 带括号:
letters %>% head()
#> [1] "a" "b" "c" "d" "e" "f"

## 不带括号:
letters %>% head
#> [1] "a" "b" "c" "d" "e" "f"

而原生管道符则***必须***带括号:

## R 4.1.0:
letters |> head()
#> [1] "a" "b" "c" "d" "e" "f"

magrittr 管道符允许在调用函数的任意位置使用“管道输入值”。管道输入值用占位符.来代替。例如,你可以在函数的第二个参数位置使用这些值:

## R 4.0.5: 字符串'at'是否能在动物名称中找到?
c("dogs", "cats", "rats") %>%
grepl("at", .)
#> [1] FALSE TRUE TRUE

原生管道符则没有提供占位符的用法。要想用原生占位符实现相同的效果,首先需要定义一个函数。你可以显式地预先定义一个函数:

## R 4.1.0
find_at = function(x) grepl("at", x)
c("dogs", "cats", "rats") |> find_at()
## [1] FALSE TRUE TRUE

…也可以隐式地使用原位函数:

## R 4.1.0: 记得右侧的括号!
c("dogs", "cats", "rats") |>
{function(x) grepl("at", x)}()
#> [1] FALSE TRUE TRUE

2.3 匿名函数的简写语法

一般情况下,R中的函数定义通常使用以下语法:

do_something = function(parameters) {
...
}

定义后的函数可以像下面这样调用:

do_something(args)

也可以使用匿名函数,这是使用高阶函数时的常见做法,例如 purrr 包中的函数(map()、reduce()、keep();以及它们的 base R 对应函数:Map() , lapply() 等)。

## 对于每一个字母:
## - 找到 'datasets' 包中以该字母开头的数据集
## 使用 'tidyverse' 语法

purrr::map(
letters[1:3],
function(x) {
ds = ls("package:datasets")
ds[stringr::str_starts(tolower(ds), x)]
}
)

#> [[1]]
#> [1] "ability.cov" "airmiles" "AirPassengers" "airquality"
#> [5] "anscombe" "attenu" "attitude" "austres"
#>
#> [[2]]
#> [1] "beaver1" "beaver2" "BJsales" "BJsales.lead" "BOD"
#>
#> [[3]]
#> [1] "cars" "ChickWeight" "chickwts" "co2" "CO2"
#> [6] "crimtab"
## base R
Map(
function(x) {
pattern = paste0("^", x) ## eg "^a" to match a leading 'a'
grep(pattern, ls("package:datasets"), value = TRUE, ignore.case = TRUE)
},
letters[1:3]
)

#> [[1]]
#> [1] "ability.cov" "airmiles" "AirPassengers" "airquality"
#> [5] "anscombe" "attenu" "attitude" "austres"
#>
#> [[2]]
#> [1] "beaver1" "beaver2" "BJsales" "BJsales.lead" "BOD"
#>
#> [[3]]
#> [1] "cars" "ChickWeight" "chickwts" "co2" "CO2"
#> [6] "crimtab"

这个情况下,function(x) {...} 的语法看上去相当啰嗦,尤其是将多个函数组合到一个管道中的时候。

R 中引入了一种新语法,可以使得声明这些匿名函数时更加简洁:

## R 4.1.0
Map(
\(x) {
pattern = paste0("^", x)
grep(pattern, ls("package:datasets"), value = TRUE, ignore.case = TRUE)
},
letters[1:3]
)

#> $a
#> [1] "ability.cov" "airmiles" "AirPassengers" "airquality"
#> [5] "anscombe" "attenu" "attitude" "austres"
#>
#> $b
#> [1] "beaver1" "beaver2" "BJsales" "BJsales.lead" "BOD"
#>
#> $c
#> [1] "cars" "ChickWeight" "chickwts" "co2" "CO2"
#> [6] "crimtab"

用这个语法,我们可以对原生管道符部分中的最后一个例子进行重写:

## R 4.1.0
c("dogs", "cats", "rats") |>
{\(x) grepl("at", x)}()
#> [1] FALSE TRUE TRUE

3 小改动

3.1 组合因子变量

在R的过去版本中,如果使用 c(factor1, factor2) 来组合两个因子向量,结果会返回一个整数向量:

## R 4.0.5: 组合不同水平的因子向量
c(factor("a"), factor("b"))
#> [1] a b
#> Levels: a b
## R 4.0.5: 组合相同水平的因子向量
c(factor("a"), factor("a"))
#> [1] a a
#> Levels: a

这是因为在R的内部,因子被储存为整数。在R >= 4.1的版本中,这种组合的结果会有一些变化:

  • 返回一个新的因子向量
  • 该因子向量的水平为原来两个因子向量水平的集合
fac1 <- factor(c("a", "b", "d"))
fac2 <- factor(c("b", "c"))
c(fac1, fac2)

#> [1] a b d b c
#> Levels: a b d c

这与 forcats::fct_c() 组合因子向量的方式一致。

3.2 使用公式语法拆分数据框

R4.1.0中加入了一种新的语法,可以方便地使用公式语法来拆分数据框。在之前的版本中,想要这么做,需要把数据框的名字输入 split() 函数:

## R 4.0.5: 将 “mtcars”数据框中的汽车按照齿轮数拆分
split(mtcars, mtcars$gear)

#> $`3`
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Hornet 4 Drive 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1
#> Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2
#> Valiant 18.1 6 225.0 105 2.76 3.460 20.22 1 0 3 1
#> Duster 360 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4
#> Merc 450SE 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3
#> Merc 450SL 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3
#> Merc 450SLC 15.2 8 275.8 180 3.07 3.780 18.00 0 0 3 3
#> Cadillac Fleetwood 10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4
#> Lincoln Continental 10.4 8 460.0 215 3.00 5.424 17.82 0 0 3 4
#> Chrysler Imperial 14.7 8 440.0 230 3.23 5.345 17.42 0 0 3 4
#> Toyota Corona 21.5 4 120.1 97 3.70 2.465 20.01 1 0 3 1
#> Dodge Challenger 15.5 8 318.0 150 2.76 3.520 16.87 0 0 3 2
#> AMC Javelin 15.2 8 304.0 150 3.15 3.435 17.30 0 0 3 2
#> Camaro Z28 13.3 8 350.0 245 3.73 3.840 15.41 0 0 3 4
#> Pontiac Firebird 19.2 8 400.0 175 3.08 3.845 17.05 0 0 3 2
#>
#> $`4`
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Mazda RX4 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4
#> Mazda RX4 Wag 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4
#> Datsun 710 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1
#> Merc 240D 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2
#> Merc 230 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2
#> Merc 280 19.2 6 167.6 123 3.92 3.440 18.30 1 0 4 4
#> Merc 280C 17.8 6 167.6 123 3.92 3.440 18.90 1 0 4 4
#> Fiat 128 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1
#> Honda Civic 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2
#> Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1
#> Fiat X1-9 27.3 4 79.0 66 4.08 1.935 18.90 1 1 4 1
#> Volvo 142E 21.4 4 121.0 109 4.11 2.780 18.60 1 1 4 2
#>
#> $`5`
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Porsche 914-2 26.0 4 120.3 91 4.43 2.140 16.7 0 1 5 2
#> Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.9 1 1 5 2
#> Ford Pantera L 15.8 8 351.0 264 4.22 3.170 14.5 0 1 5 4
#> Ferrari Dino 19.7 6 145.0 175 3.62 2.770 15.5 0 1 5 6
#> Maserati Bora 15.0 8 301.0 335 3.54 3.570 14.6 0 1 5 8

使用magrittr管道时,语法就显得有些“笨重”:

## R 4.0.5
## 分别获取齿轮数为3,4,5的汽车中,每加仑汽油行驶距离最长的汽车(mpg)

mtcars %>%
split(., .$gear) %>%
lapply(function(x) x[which.max(x$mpg),])

#> $`3`
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Toyota Corona 21.5 4 120.1 97 3.7 2.465 20.01 1 0 3 1
#>
#> $`4`
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.9 1 1 4 1
#>
#> $`5`
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.9 1 1 5 2

将新的管道语法和函数简写与新的split语法结合起来,可以改写如下:

## R 4.1.0
mtcars |>
split(~ gear) |>
lapply(\(x) x[which.max(x$mpg), ])

#> $`3`
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Toyota Corona 21.5 4 120.1 97 3.7 2.465 20.01 1 0 3 1
#>
#> $`4`
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.9 1 1 4 1
#>
#> $`5`
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.9 1 1 5 2

感觉简洁了许多~

4. 使用 hotkeys 设置原生管道符快捷键

R 4.1.0 中并没有为原生管道符|>快捷键,但我们可以使用 hotkeys 包来自定义我们的快捷键。

可以使用以下的方法安装:

## 从 GitHub 上安装:
devtools::install_github('r-stata/hotkeys')
## 从附件中的源码安装:
devtools::install_local('hotkeys_0.2.0.tar.gz')

然后就可以从 RStudio 界面上点击 Tools -> Modify Keyboard Shortcuts 进入快捷键设置界面,输入 insert 找到输入类快捷键的设置:

使用 hotkeys 设置原生管道符快捷键

然后就可以设置 |> 的快捷键了,当然,其他各种操作符的快捷键可以用 hotkeys 包一起设置~

点击这里跳转到 RStata 短书平台获取附件:R 4.1.0 的新特性 & 使用 hotkeys 配置输入快捷键

评论