函数

函数是R的基石:要想掌握这本书中更多的高级技能,你必须对函数运行了如指掌。也许你已经写了不少 R 语言函数,也熟悉了函数运行的基本知识。这一章的主要目的,是帮助你将已有的不成体系的函数知识,转化为对函数本质和函数如何运行的牢固知识体系。在这一章里,你将见识到不少有趣的小窍门,但是你将学到的大多数东西更重要,因为它们是学习高级技能的基础。functions

理解R最重要的一点是,函数自身也是对象。你可以像处理其他类型的对象一样处理函数。关于函数会在函数式编程进行更加深入的讨论。

测试

做做这个简单的测试看看你是否需要阅读本章内容。如果你能很快地得到答案,可以轻松地跳过本章。本章最后提供参考答案

  1. 函数的三大组成部分是什么?

  2. 下面代码的返回值是什么?

    x <- 10
    f1 <- function(x) {
      function() {
        x + 10
      }
    }
    f1(1)()
    
  3. 如何用更通俗的方式编写下面的代码?

    `+`(1, `*`(2, 3))
    
  4. 如何用更易读的方式调用这个函数?

    mean(, TRUE, x = c(1:10, NA))
    
  5. 这个函数在调用是是否会报错?为什么?

    f2 <- function(a, b) {
      a * 10
    }
    f2(10, stop("This is an error!"))
    
  6. 什么是中缀函数(infix functions)?如何编写?什么是置换函数(replacement functions)?如何编写?

  7. 什么函数能确保无论函数如何终止都执行清除指令?

大纲
  • 函数组成 介绍函数的三大组成部分。

  • 作用域 告诉你R如何从名称中找到值,作用域的过程。

  • 操作即调用 R中的一切都是函数调用的结果,即使看起来一点儿也不像。

  • 函数参数 讨论将参数传递到函数的三种方式,如何调用给定参数列表的函数,以及延迟求值的作用。

  • 特殊调用 介绍两种特殊的函数:中缀函数和替代函数(infix and replacement functions)。

  • 返回值 讨论函数如何以及何时返回结果,还将探讨如何确保函数在退出之前进行某些操作。

先决条件

你唯一需要的包是 pryr, 当修改对应位置的向量时会它可以告诉我们发生了什么。安装命令为 install.packages("pryr")

函数组成

所有的R函数都有三个部分:

  • 主体 body(),函数内部的代码。

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

  • 环境 environment(),函数变量的位置映射。

当你在R中打印一个函数,就会看到这三个组成部分。如果没有显示环境,那么就意味着这个函数是在全局环境中被创建的。

f <- function(x) x^2
f
#> function(x) x^2

formals(f)
#> $x
body(f)
#> x^2
environment(f)
#> <environment: R_GlobalEnv>

body()formals()environment() 的赋值形式也能用于修改函数。

和R中的所有对象一样,函数也能拥有任意数量额外属性 attributes()。R中的一种基础属性是“源码引用”(srcref,source reference的简称),用来指向创造函数的源代码。与body()不同,属性可以包含代码注释和其他格式。你也可以在函数中添加属性。比如,你可以在print()函数中添加类属性class()来定制打印方法。\index{functions!attributes}

原函数

函数包含三个组件,这一规则只有一个例外。原函数(primitive functions),比如sum(),直接使用.Primitive()调用C语言代码而不包含任何R代码。因此,它的三个组件均为null

primitive functions

sum
formals(sum)
body(sum)
environment(sum)

原函数只存在于基础包(base)中,因为它们是在底层进行运算,所以运算效率更高(原函数的置换函数不需要复制),参数匹配规则也不同(如switchcall)。这使得原函数与R中的其他函数的表现不一样。因此除非别无他法,R核心团队通常避免创建原函数。

练习

1.什么函数能够帮助你判断一个对象是不是函数?什么函数能够帮助你判断一个函数是不是原函数?

2.这段代码创建了一个列表,里面包含了基础包中的所有函数。

objs <- mget(ls("package:base"), inherits = TRUE)
funs <- Filter(is.function, objs)

根据它来回答下列问题:

a.哪一个基础包中函数的参数最多?

b.有多少基础包中的函数没有参数?这些函数有什么特点?

c.你能修改这段代码,找出所有的原函数吗?

3.函数的三个组件是什么?

4.当打印一个函数时没有显示环境,那么它是在什么环境中被创建的?

作用域

作用域是控制R如何查找值的规则的集合。下例中,作用域就是,R如何应有这些规则,让符号x找到它的值10: \index{scoping!lexical|see{lexical scoping}} \index{lexical scoping}

x <- 10
x

理解作用域可以帮助你:

  • 通过创建函数构建工具,详见 函数式编程

  • 重新覆盖常用的参数传递规则,进行非标准参数传递(non-standard evaluation),详见 non-standard evaluation

R 语言有两种类型的作用域:静态作用域,在语言层面自动实现;动态作用域,在交互分析中用于选择函数,可以节省打字。在这里我们只讨论静态作用域,因为它是在函数创建时就确定了的。动态作用域详见 scoping-issues

静态作用域查找符号的值是根据函数在创建时是如何嵌套的,而不是在调用的时候。在静态作用域下,你不必知晓函数是如何被调用的就可以知道去哪里找变量的值。只用看看函数的定义就行了。

静态作用域(lexical scoping)中“lexical”一词并不是通常英语中的定义(“为了区分一门语言的语法和结构而与单词或词汇相关的”),而是计算机科学术语“词法分析”(lexing),将文本代码转化为计算机可以理解的片段。

R语言的静态作用域实现有四个基本规则:

  • 名称覆盖(name masking)
  • 函数 vs. 变量
  • 初始化(a fresh start)
  • 动态查找(dynamic lookup)

也许你已经知道了这些原则,但你可能还没有仔细想过。在脑海中运行代码,记得在那之前不要看答案。

名称覆盖

下面的例子阐释了静态作用域的最基本原则,预测下面的输出值应该毫无压力。

```{r, eval = FALSE} f <- function() { x <- 1 y <- 2 c(x, y) } f() rm(f)

如果在函数内部名称没有被定义,R会在上一层级里查找。

```{r, eval = FALSE}
x <- 2
g <- function() {
  y <- 1
  c(x, y)
}
g()
rm(x, g)

这一规则同样适用于在另外一个函数内部定义的函数:先在当前函数内部查找,然后在函数被创建的环境中查找,以此类推,直到全局环境,最后到其他被加载的包中查找。在脑海中运行代码,然后运行R代码,确认答案。

{r, eval = FALSE} x <- 1 h <- function() { y <- 2 i <- function() { z <- 3 c(x, y, z) } i() } h() rm(x, h) 这一规则也适用于闭合环境,

results matching ""

    No results matching ""