Advanced R学习笔记(五)Functions

介绍

如果你读到了这本书,那么你应该已经编辑了很多R函数了(现在有了 2020/06/05),并且知道怎么使用它们去减少代码的冗余.在本章中将系统地学习如何更加深入地理解它们.同时也会看到一些有趣的技巧和技术.这一章中学到的东西对于理解本书后续部分将会很重要.

函数基本原理

想要理解R中的函数需要内化两个重要的概念

  • 函数是可以被分解为三个部分的: 参数,主题和环境

但也有例外,有一小部分基本函数是完全使用C完成的

  • 函数是一个对象,一个作为向量的对象

函数的部件

函数有三个部分组成

  • formals() 控制如何调用函数的参数列表

  • body() 函数内部的代码

  • environment() 确定函数如何查找与名称关联的值的数据结构

参数和主体在创造一个函数时被明确地指定,环境则是被含蓄地指定,这基于在哪里定义这个函数.函数的环境一直存在,但是它仅在函数不是被定义在全局环境的情况下被打印,


f02 <- function(x, y) {

  # A comment

  x + y

}

formals(f02)

#> $x

#> 

#> 

#> $y

body(f02)

#> {

#> x + y

#> }

environment(f02)

#> <environment: R_GlobalEnv>

创造一个函数,如下方的那个图.那个左侧的黑点是环境,两个右侧的方块是函数的参数.并没有画出主体,因为它经常很大,并不能帮助理解函数的形状.

image

像R中的所有对象一样,函数也可以具有额外的attributes().基础R的一个属性是"srcref".是source reference的缩写.这指向用于创建函数的源代码.srcref被用来打印,因为它不像body(),它包含了代码的注释和其他的格式.attr(f02, "srcref")


#> function(x, y) {

#> # A comment

#> x + y

#> }

基础函数

这些函数是由C代码直接调用的,并不具有三个结构,如sum()和[


sum

#> function (..., na.rm = FALSE) .Primitive("sum")

`[`

#> .Primitive("[")

它们有两种形式"builtin"和"special"


typeof(sum)

#> [1] "builtin"

typeof(`[`)

#> [1] "special"

这些函数主要存在于C而不是R,所以formals() body()和environment()都是NULL


formals(sum)

#> NULL

body(sum)

#> NULL

environment(sum)

#> NULL

基础的函数仅被用于基础包,它们具有性能优势,这种优势的代价就是更加难以编写.所以R代码一般避免去创建它们,除非没有别的选择.

第一类函数

理解R函数是对象是非常重要的,这是一种语言性质,被叫做第一类函数.不想其他的语言,没有特殊的语法去定义和命名函数,可以使用function()去创造一个函数对象,并使用<-将它绑定给一个名字.


f01 <- function(x) {

  sin(1 / x ^ 2)

}

image

当创建一个函数之后将其绑定一个名字时,这个绑定的步骤并不是必需的.如果选择不给这个函数一个名字,将会得到一个匿名的函数(anonymous function).当不值得花力气去 想一个名字的时候是有用的.


lapply(mtcars, function(x) length(unique(x)))

Filter(function(x) !is.numeric(x), mtcars)

integrate(function(x) sin(x) ^ 2, 0, pi)

或者也可以把函数放在一个列表中


funs <- list(

  half = function(x) x / 2,

  double = function(x) x * 2

)

funs$double(10)

#> [1] 20

在R中,将会经常看到一个函数被叫做闭包(closures)这个名字反射出了R函数捕获或封装它们的环境的事实, 将会在7.4.2介绍

调用一个函数

正常地调用一个函数通过用括号括起来的参数:mean(1:1-,na.rm = TRUE).但是当函数已经存在于数据结构中会怎么样呢


args <- list(1:10, na.rm = TRUE)

可以使用do.call()来替代,它有两个参数:被调用的函数和一个包含函数参数的列表


do.call(mean, args)

#> [1] 5.5

练习

调用一个匿名函数


function(x) 3()

#> function(x) 3()

(function(x) 3)()

#> [1] 3

复合函数

基础R提供了两种方法去构成多函数调用,例如,想要用sqrt()和mean()来构建一个闭块


square <- function(x) x^2

deviation <- function(x) x - mean(x)

也可以嵌套函数调用


x <- runif(100)

sqrt(mean(square(deviation(x))))

#> [1] 0.274

或者将中间结果保存为变量


out <- deviation(x)

out <- square(out)

out <- mean(out)

out <- sqrt(out)

out

#> [1] 0.274

magrittr包提供了第三种方法,它提供了一个二元操作符%>%,它调用一个管道,读作"and then"


library(magrittr)

x %>%

  deviation() %>%

  square() %>%

  mean() %>%

  sqrt()

#> [1] 0.274

x%>%f()等于f(x), x%>%f(y)等于f(x,y). 这个管道允许你关注函数的高级组合,而不是低级的数据流.重点是在做什么,而不是在修饰什么.这种风格在Haskell和F#中很常见

这三种选择各有优劣

  • 嵌套 f(g(x)) 时间明的,并且适合于短的结构,但是稍长的结构将会很难读懂,因为他们是从里到外,从右到左的.因此,参数可能被分离到很远的地方.

  • 中间的对象 y <- f(x); g(y) 需要命名中间对象.当对象很重要时,这是很费力的,当值真的只是中间产物时就会很轻松.

  • 管道 x %>% f() %>% g()允许直接的从左到右地阅读代码,而不需要命名中间的对象.但是这只能是线性的并且只能转换一个对象,它也需要额外的第三方包并且嘉定读者能够理解管道

大部分的代码是使用这三种类型的组合,管道是最常用的分析代码,分析的内容都由一个对象的一系列转换而来.

词法作用域

在Names and values中,我们讨论了分配,就是把一个名字绑定给一个值。这里要讨论一下作用域,就是查找值绑定的名字的行为。它的基础规则是非常直观的,即使你没有研究过,但是你可能以及视其为自然了.例如,下面的代码会返回什么?


x <- 10

g01 <- function() {

  x <- 20

  x

}

g01()

在这一部分,将会学到作用域的正式规则以及一些更加细节的东西.对于作用域更深入的理解将会有助于使用更高级的编程工具,最终使得可以写一个工具转换R代码到其他的语言.

R语言使用词法作用域:它查找一个名字对应的值是基于函数如何被定义的.而不是它如何被调用."Lexical"在这并不是英语的形容词——"词汇或单词相关的".它是一个CS术语,告诉我们作用域是一个解析时的结构而不是一个运行时的结构.

R的词法作用域有四条主要的规则:

  • 名字掩盖

  • 功能 vs 变量

  • 新的起点

  • 动态查询

名字屏蔽

词法作用域的基础规则是函数内部定义的名称覆盖函数外部定义的名称.如下例所示.


x <- 10

y <- 20

g02 <- function() {

  x <- 1

  y <- 2

  c(x, y)

}

g02()

#> [1] 1 2

如果名字并没有在函数内部被定义,R将会向上查找


x <- 2

g03 <- function() {

  y <- 1

  c(x, y)

}

g03()

#> [1] 2 1

# And this doesn't change the previous value of y

y

#> [1] 20

如果一个函数被定义在另一个函数之中,也适用于同样的规则.首先,R会查找当前函数的内部,之后将会查找这个函数被定义的位置(知道全局环境).最终它会去查找其他已经加载的包.


x <- 1

g04 <- function() {

  y <- 2

  i <- function() {

    z <- 3

    c(x, y, z)

  }

  i()

}

g04()

同样的规则也适用于由其他函数创造的函数,

函数 vs 变量

在R中函数是一个普通的对象.这意味着作用域的规则也适用于函数


g07 <- function(x) x + 1

g08 <- function() {

  g07 <- function(x) x + 100

  g07(10)

}

g08()

#> [1] 110

而且,当一个函数和一个非函数共享同一个名字(当然是在不同的环境里).这些规则的应用会更加复杂一点.当函数调用中使用一个名字,R在查找这个值的时候,将会忽略非函数对象,例如:


g09 <- function(x) x + 100

g10 <- function() {

  g09 <- 10

  g09(g09)

}

g10()

#> [1] 110

最好去避免对不同的东西使用同一个名字.

新的起点

函数调用之间的值会怎么样.看下例:


g11 <- function() {

  if (!exists("a")) {

    a <- 1

  } else {

    a <- a + 1

  }

  a

}

g11()

g11()

两次都返回同一个值是因为每一次函数被调用都会创建一个新的环境去承载它的执行.这意味着一个函数无法告诉你上一次运行后发生了什么,每一次调用都是完全独立的.

动态查询

词法作用域决定在何处查找值而不是在何时查找值.当函数运行时,R查找值,而不是当其被创建时.同时,这两个性质告诉我们,函数的输出可以根据外界环境的对象的不同而输出不同的值.


g12 <- function() x + 1

x <- 15

g12()

#> [1] 16

x <- 20

g12()

#> [1] 21

这种行为是极其讨厌的,如果代码中有一个拼写错误,当创建一个函数式,不会得到一个错误提示信息.根据在全局环境中定义的变量,在运行函数时可能不会得到一个错误提示.

为了探查这个问题,可以使用codetools::findGlobals().这个函数列出了函数所有的外部依赖项


codetools::findGlobals(g12)

#> [1] "+" "x"

为了解决这个问题,可以手动地改变函数的环境到emtyenv()(一个空环境)


environment(g12) <- emptyenv()

g12()

#> Error in x + 1:

#> could not find function "+"

这个问题和解决方案解释了为什么看似不好的行为的存在. R依赖于词法作用域去寻找一切,从显而易见的mean()到不那么明显的如+和{都是如此.这给了R词法作用域规则一个相当漂亮的简单性.

惰性计算

在R之后,函数的参数是惰性计算的:它们只有在被访问的时候才会被计算.例如,下面的代码就不会产生一个错误,因为参数"x"并没有被用到.


h01 <- function(x) {

  10

}

h01(stop("This is an error!"))

#> [1] 10

这是一个重要的特性,因为这允许你将潜在的"昂贵的"计算包含在其中,并且只有当用到它的时候才会去计算.

Promises

惰性计算是被一种被称为promise的数据结构或者(不常见的)thunk

这种特性使得R成为这么有趣的语言.

promis由三部分组成

  • 一个表达式,如x + y, 这引起了延迟计算.

  • 应该计算的环境,即调用函数的环境,这确保了下面的函数返回11而不是101


y <- 10

h02 <- function(x) {

  y <- 100

  x + 1

}

h02(y)

#> [1] 11

这也意味着当在调用一个变量的时候赋值,这个变量将会被绑定在函数外部而不是函数内部


h02(y <- 1000)

#> [1] 1001

y

#> [1] 1000

  • 一个值,当表达式在制定环境中求值时,被计算并且在第一次访问promise时被储存.这确保promise只被计算一次,这也是为什么下面只显示一次calculating

double <- function(x) { 

  message("Calculating...")

  x * 2

}

h03 <- function(x) {

  c(x, x)

}

h03(double(x))

#> Calculating...

#> [1] 40 40

不能使用R代码去操控promises.promises更像一个量子状态:任何尝试检测它们的R代码豆浆杯立即强制评估,使得promise消失.

默认参数

由于惰性计算,默认值可以用其他参数来` 定义,甚至可以用函数中稍后定义的变量


h04 <- function(x = 1, y = x * 2, z = a + b) {

  a <- 10

  b <- 100

  c(x, y, z)

}

h04()

#> [1] 1 2 110

许多基础的R函数使用了这一技巧,但是并不推荐使用它.这使得代码很难以理解:为了预测将会返回什么,需要做的确切的默认参数的计算顺序

默认参数和用户提供的参数的计算环境是略有不同的,因为默认参数是在函数中计算的.这意味着看似相同的东西会产生不同的结果.下面有一个极端的例子:


h05 <- function(x = ls()) {

  a <- 1

  x

}

# ls() evaluated inside h05:

h05()

#> [1] "a" "x"

# ls() evaluated in global environment:

h05(ls())

#> [1] "h05"

缺失的函数

要确定函数的值是来自于默认还是用户,可以使用missing()


h06 <- function(x = 10) {

  list(missing(x), x)

}

str(h06())

#> List of 2

#> $ : logi TRUE

#> $ : num 10

str(h06(10))

#> List of 2

#> $ : logi FALSE

#> $ : num 10

missing()最好少用,以sample()为例


args(sample)

#> function (x, size, replace = FALSE, prob = NULL) 

#> NULL

看上去x和size是被需要的,但是事实上如果没有提供,将会使用missing()来提供size的默认值.应该这样改写


sample <- function(x, size = NULL, replace = FALSE, prob = NULL) {

  if (is.null(size)) {

    size <- length(x)

  }

  x[sample.int(length(x), size, replace = replace, prob = prob)]

}

使用创建的二元操作符%||%(如果左侧不是空的就使用左侧,否则使用右侧),可要进一步简化sample()


`%||%` <- function(lhs, rhs) {

  if (!is.null(lhs)) {

    lhs

  } else {

    rhs

  }

}

sample <- function(x, size = NULL, replace = FALSE, prob = NULL) {

  size <- size %||% length(x)

  x[sample.int(length(x), size, replace = replace, prob = prob)]

}

因为惰性计算,所以不需要担心不必要的计算,右边的计算只有当左边是NULL时才会进行

测试

image
image
image

点点点

函数有一个特殊的参数... 有了它,函数可以接受任意数量的附加参数.在其他的编程语言中,这种参数经常被叫做varargs(不定参数),使用它的函数叫做变进函数.也可以使用这个参数来将额外的参数附加给别的函数


i01 <- function(y, z) {

  list(y = y, z = z)

}

i02 <- function(x, ...) {

  i01(...)

}

str(i02(x = 1, y = 2, z = 3))

#> List of 2

#> $ y: num 2

#> $ z: num 3

使用一个特殊的形式..N,它可以通过位置引用...


i03 <- function(...) {

  list(first = ..1, third = ..3)

}

str(i03(1, 2, 3))

#> List of 2

#> $ first: num 1

#> $ third: num 3

更有用的是list(...),它计算参数,并储存到一个列表中


i04 <- function(...) {

  list(...)

}

str(i04(a = 1, b = 2))

#> List of 2

#> $ a: num 1

#> $ b: num 2

有两种...的主要使用方法

  • 如果你的函数以一个函数为参数,可能会想通过某种方式来添加额外的参数给那个函数.在这个例子中lapply()使用...来将na.rm传递给mean()

x <- list(c(1, 3, NA), c(4, NA, 6))

str(lapply(x, mean, na.rm = TRUE))

#> List of 2

#> $ : num 2

#> $ : num 5

  • 如果你的函数使用了S3类,将会需要使类的方法来接受任意额外的参数.例如,使用print()函数.因为,有许多不同的选项来打印不同类型的对象.,没有办法预先指明每个可能的参数.点点点是允许个别的方法拥有不同的参数.

print(factor(letters), max.levels = 4)

print(y ~ x, showEnv = TRUE)

使用...用两个缺点

  • 当使用它去传递参数给别的函数时,不得不仔细地给用户表述这些参数去哪了.这使得很难理解lapply(),plot()等函数如何使用.

  • 一个拼错的参数将不会产生错误,这使得拼写错误很容易被忽视.


sum(1, 2, NA, na_rm = TRUE)

#> [1] NA

退出函数

大部分的函数使用这两种方法中的一种来退出:它们返回一个值表示成功或者是扔出一个错误,表示失败.

在这一部分主要描述返回一个值(隐式或显式,可见或不可见),短暂的描述一下错误,并介绍一下退出操作,这使得在函数退出的情况下运行代码.

显式和隐式返回

一个函数返回一个值有两种方法

  • 隐式的,最后一个被计算的表达就是返回值

j01 <- function(x) {

  if (x < 10) {

    0

  } else {

    10

  }

}

j01(5)

#> [1] 0

j01(15)

#> [1] 10

  • 显式返回是通过调用return()

j02 <- function(x) {

  if (x < 10) {

    return(0)

  } else {

    return(10)

  }

}

不可见的值

大部分的函返回一个可见的值,在交互式上下文中调用函数将返回一个结果


j03 <- function() 1

j03()

#> [1] 1

但是,可以通过对最后一个值使用invisible()来防止自动打印


j04 <- function() invisible(1)

j04()

为了确定这个值存在,可以显式的打印,或者是用括号括起来


print(j04())

#> [1] 1

(j04())

#> [1] 1

或者可以使用withVisible()来返回一个值或是可见的标志


str(withVisible(j04()))

#> List of 2

#> $ value : num 1

#> $ visible: logi FALSE

最常见的函数的隐式返回就是<-


a <- 2

(a <- 2)

#> [1] 2

这使得连锁分配成为可能


a <- b <- c <- d <- 2

通常,任何主要使用副作用的函数(如<-,print(),plot())都应该返回一个不可见的值(通常是第一个参数的值).

错误

如果一个函数不能完成分配给它的任务,它就会通过stop()扔出一个错误,这回理科终止这个函数的执行


j05 <- function() {

  stop("I'm an error")

  return(10)

}

j05()

#> Error in j05():

#> I'm an error

错误表明某些东西出了错,强制用户去解决这些问题,有的语言(如C,Go和Rust)依赖于特殊的返回值来指明错误,但是R总是会抛出一个错误.

退出操作

有时一个函数需要变为全局的状态,但是必须清理这些变化是很痛苦的.为了确保这些变化被撤销,并且无论函数如何退出,全局状态都可以恢复,使用on.exit()来设置退出操作.下面的简单的示例展示了退出操作是正常退出还是出现了错误,都将运行退出处理程序


j06 <- function(x) {

  cat("Hello\n")

  on.exit(cat("Goodbye!\n"), add = TRUE)

  if (x) {

    return(10)

  } else {

    stop("Error")

  }

}

j06(TRUE)

#> Hello

#> Goodbye!

#> [1] 10

j06(FALSE)

#> Hello

#> Error in j06(FALSE):

#> Error

#> Goodbye!

当使用on.exit()时应该总是设置add = TRUE,如果不这么做,每次调用on.exit()都会覆盖上一个退出操作.即使只使用了一个退出处理,也应该养成设置add = TRUE的好习惯,来让自己不要在之后设置更多退出处理的时候得到一个不令人愉悦的惊喜.

on.exit()是很有用的,因为它允许你将清理代码直接放在需要清理的代码旁边


cleanup <- function(dir, code) {

  old_dir <- setwd(dir)

  on.exit(setwd(old_dir), add = TRUE)

  old_opt <- options(stringsAsFactors = FALSE)

  on.exit(options(old_opt), add = TRUE)

}

加上延迟计算,这为在更改的环境中运行代码块创建了一个非常有用的模式


with_dir <- function(dir, code) {

  old <- setwd(dir)

  on.exit(setwd(old), add = TRUE)

  force(code)

}

getwd()

#> [1] "/home/travis/build/hadley/adv-r"

with_dir("~", getwd())

#> [1] "/home/travis"

这里使用force()并不是一定要使用的,因为只是引用code就会求值.但是,是哦用force)()会很清楚,我们故意强迫执行的.

withr包提供了一批不同的函数来设置一个暂时的状态.

在R3.4和更早的版本中,on.exit()表达式总是按照创建的顺序运行.


j08 <- function() {

  on.exit(message("a"), add = TRUE)

  on.exit(message("b"), add = TRUE)

}

j08()

#> a

#> b

这可能会使得清理变得有些棘手,如果一些行动需要特殊的顺序,典型的就是想要最新添加的表达式最先运行.在R3.5即以后可以通过设置after = FALSE来控制


j09 <- function() {

  on.exit(message("a"), add = TRUE, after = FALSE)

  on.exit(message("b"), add = TRUE, after = FALSE)

}

j09()

#> b

#> a

函数形式

虽然R中所有的事情发生都是函数调用的结果,但是并不是所有的调用都是一样的.函数的调用被分为四种:

  • 前缀: 函数名在它的参数的前面,像是foofy(a,b,c),这组成了R中大部分的函数调用.

  • 插入: 这种函数的名字在它的参数的中间.例如x + y.插入的形式被用在很多的数学操作符中,已经用户定义的以%开头和结尾的函数

  • 替换: 用参数替换值的函数.如names(df) <- c("a","b","c").它看起来像是前缀的函数

  • 特殊类: 像是[[,if和for.虽然它们没有一致的结构,但是都在R的语法中扮演着重要的角色.

虽然有四种,但是实际上只需要一种,因为所有的调用都可以写成前缀型.

改写为前缀形式

一个有趣的性质是R中所有的插入,替换,特殊形式的函数都可以被改写为前缀形式.这样做是非常有用的,因为这可以帮助理解语言的结构,这给了每个函数一个真实的名字,使得为了乐趣和需求来改变这些函数.

下面的例子展示了三对相同的调用,重写为前置的形式.


x + y

`+`(x, y)

names(df) <- c("x", "y", "z")

`names<-`(df, c("x", "y", "z"))

for(i in 1:10) print(i)

`for`(i, 1:10, print(i))

令人吃惊的是,在R中,for能够像一个正则函数一样被调用.对于R中的每一个操作都是一样的,这意味着知道每个非前缀函数的名字就可以改写它的行为.例如,如果你心情不好,并且朋友不在身边时,就可以引入这样一个有趣的bug,在10%的情况下,会使得括号内的数+1


`(` <- function(e1) {

  if (is.numeric(e1) && runif(1) < 0.1) {

    e1 + 1

  } else {

    e1

  }

}

replicate(50, (1 + 2))

#> [1] 3 3 3 3 3 3 3 3 3 3 3 3 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3

#> [36] 3 3 3 4 3 4 3 3 3 3 4 3 3 3 3

rm("(")

当然,重写内置函数并不是一个好主意,但是通过一些操作可以使得它仅应用于某些代码块.这为编写特定领域的语言和其他语言的翻译程序提供了一种干净而优雅的方法。

在使用函数式编程工具时,一个更有用的应用出现了.例如"可以使用sapply()来给列表中每个元素添加三,首先要先定义一个函数add()


add <- function(x, y) x + y

sapply(1:10, add, 3)

#> [1] 4 5 6 7 8 9 10 11 12 13

但是也可以以现存的函数+来简化过程


sapply(1:5, `+`, 3)

#> [1] 4 5 6 7 8

前缀形式

前缀形式是R代码中最常用的形式,事实上在大部分编程语言中都是.前缀调用在R中有一点点特殊,因为可以以三种方式指定参数.

  • 通过位置,如help(mean)

  • 使用部分的匹配,如help(top = mean)

  • 通过名字,如help(topic = mean)

如下面的块展示的,参数由精确的名称,然后是唯一的前缀,最后才是位置来匹配


k01 <- function(abcdef, bcde1, bcde2) {

  list(a = abcdef, b1 = bcde1, b2 = bcde2)

}

str(k01(1, 2, 3))

#> List of 3

#> $ a : num 1

#> $ b1: num 2

#> $ b2: num 3

str(k01(2, 3, abcdef = 1))

#> List of 3

#> $ a : num 1

#> $ b1: num 2

#> $ b2: num 3

# Can abbreviate long argument names:

str(k01(2, 3, a = 1))

#> List of 3

#> $ a : num 1

#> $ b1: num 2

#> $ b2: num 3

# But this doesn't work because abbreviation is ambiguous

str(k01(1, 3, b = 1))

#> Error in k01(1, 3, b = 1):

#> argument 3 matches multiple formal arguments

通常来说,使用为只匹配仅用于前一个或两个参数,它们是最常用的,大部分读者都知道它们是什么.应该避免用位置去匹配不常用的参数,并且永远不要用部分匹配.不幸的是,并不能禁用部分匹配,但是可以使用warnPartialMatchArgs 选项,来使其发出警告


options(warnPartialMatchArgs = TRUE)

x <- k01(a = 1, 2, 3)

#> Warning in k01(a = 1, 2, 3): partial argument match of 'a' to 'abcdef'

插入函数

插入函数的名字来源于函数名在它的参数之间,因此它们有两个参数.R之中有许多的插入函数操作符::, ::, :::, $, @, ^, , /, +, -, >, >=, <, <=, ==, !=, !, &, &&, |, ||, ~, <-和<<-.也可以创造自己的插入函数(开头和结尾都是%).基础的R使用这种模式定义了%%,%%,%?%,%in%,%o%和%x%.

定义自己的插入函数是很简单的,可以创建一个两个参数的函数,并且将它们绑定到一个开头和结尾都是%的名字上面


`%+%` <- function(a, b) paste0(a, b)

"new " %+% "string"

#> [1] "new string"

插入函数的名字比规律的R函数更加灵活,它们能够包含任何的字符,除了%.在定义函数的时候你需要转义特殊的字符,但是在函数调用的时候不需要.


`% %` <- function(a, b) paste(a, b)

`%/\\%` <- function(a, b) paste(a, b)

"a" % % "b"

#> [1] "a b"

"a" %/\% "b"

#> [1] "a b"

R的默认优先规则是插入操作符由左到右组成


`%-%` <- function(a, b) paste0("(", a, " %-% ", b, ")")

"a" %-% "b" %-% "c"

#> [1] "((a %-% b) %-% c)"

有两个特殊的中间函数,能够被一个参数调用


-1

#> [1] -1

+10

#> [1] 10

替换函数

替换函数类似于在适当的位置修改参数,并且有特殊的模式xxx<-.它们必须有一个有名字的x和value,必须返回修饰过后的对象.例如,下面的函数修饰第二个元素.


`second<-` <- function(x, value) {

  x[2] <- value

  x

}

替换函数的使用方式是将函数调用放在<-的左边


x <- 1:10

second(x) <- 5L

x

#> [1] 1 5 3 4 5 6 7 8 9 10

它们的行为就是在在这里修改参数一样,但是实际上它们是创建了一个修改之后的拷贝.可以使用tracemem()看到


x <- 1:10

tracemem(x)

#> <0x7ffae71bd880>

second(x) <- 6L

#> tracemem[0x7ffae71bd880 -> 0x7ffae61b5480]: 

#> tracemem[0x7ffae61b5480 -> 0x7ffae73f0408]: second<- 

如果置换函数需要额外的参数,应该将它们放在x和value的中间,然后在左边调用额外的参数


`modify<-` <- function(x, position, value) {

  x[position] <- value

  x

}

modify(x, 1) <- 10

x

#> [1] 10 5 3 4 5 6 7 8 9 10

当写modify(x, 1) <- 10,其实会变成x <- modify<-(x, 1, 10)

将替换与其他函数结合需要更加复杂的转化,例如:


x <- c(a = 1, b = 2, c = 3)

names(x)

#> [1] "a" "b" "c"

names(x)[2] <- "two"

names(x)

#> [1] "a" "two" "c"

被转化为


`*tmp*` <- x

x <- `names<-`(`*tmp*`, `[<-`(names(`*tmp*`), 2, "two"))

rm(`*tmp*`)

它创建了一个tmp的局部变量,然后被删除了

特殊形式

最后有一堆语言特征通常以特殊的方式编写,但也有前缀形式。这些包括括号:


(x) (`(`(x))

{x} (`{`(x)).

建造子集的操作符


x[i] (`[`(x, i))

x[[i]] (`[[`(x, i))

还有一些控制结构


if (cond) true (`if`(cond, true))

if (cond) true else false (`if`(cond, true, false))

for(var in seq) action (`for`(var, seq, action))

while(cond) action (`while`(cond, action))

repeat expr (`repeat`(expr))

next (`next`())

break (`break`())

最后,最复杂的是function函数


function(arg1, arg2) {body} (`function`(alist(arg1, arg2), body, env))

知道特殊形式的函数的名字杜宇得到它们的文档是非常有用的.?(在语法上是错误的,可以使用?(来得到帮助文档

所有的特殊形式的函数都使用基础函数编写的,这意味着打印这些函数是没有什么信息的


`for`

#> .Primitive("for")

练习

image
image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349