轨道
/
R
R
/
练习
/
坐标变换
坐标变换

坐标变换

学习练习

简介

函数最早是在基础这个概念里介绍的,当时用了这样的例子:

squareit <- function(x) {
    x * x
}

squareit(3)
#> [1] 9

# shorter form
squareit_short <- function(x) x ^ 2

仔细看看squareit的定义,可以分辨出几个部分:

  • 函数接受一个形参x。
  • 它有一个函数体,通常写在花括号{ }里。
  • 函数对象一旦创建,就会被赋值给变量squareit。

在 R 里,函数是一等对象,和数字、字符串没什么两样。因此,squareit <- function...这个赋值在语法上和x <- 42完全一样。

实参

与 Python、Julia 等其他脚本语言不同,R 并不明确区分位置实参和关键字实参。

函数调用既可以按位置传值,也可以按名称传值。后者适合实参很多、顺序又不容易记住的复杂函数。

f <- function(x, y) x / y

# call positionally
f(4, 2)
#> [1] 2

# call by name
f(y = 2, x = 4)
#> [1] 2

可选形参

可以在函数定义中指定形参的默认值,但默认值必须放在所有没有默认值的形参之后。

这样我们就能选择是接受默认值,还是覆盖它。

g <- function(x, y = 2) x / y

# default y value
g(6)
#> [1] 3

# explicit y value
g(6, 3)
#> [1] 2

额外实参

想接受任意数量的额外实参,可以在定义里使用...(省略号)。函数调用中多出来的值可以转换成一个向量,不过请继续往下读,还有一种使用这些“点参数”的方式(在其他一些语言中称为“varargs”)。

var_f <- function(x, y, ...) {
  print(c(...))
}

var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"

函数环境

前面我们说过,形参和函数体都是函数的组成部分。

实际上还有第三个组成部分:函数被定义时所处的环境。

嵌套函数就是一个很好的例子:

outer_func <- function(x) {
  inner_func <- function(y) {
    x * y
  }
  
  inner_func(3)
}

outer_func(5)
#> [1] 15

这次函数调用把x = 5传给外层函数,这个值在该函数体内是可以访问的。

内层函数是外层函数体的一部分,因此能访问x的值。换句话说,x = 5位于内层函数的环境中。

从技术上说,这叫做闭包。

环境对点参数尤其重要,因为通过这种方式提供的任何值,都可以透传给函数体里的函数调用。外层函数不需要知道,也不必关心这些点参数是什么意思。

f_var <- function(x, ...) {
  sum(x, ...)
}

x <- c(1, 2, NA, 6)

# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA

# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9

Tidyverse 的库(比如stringr)大量使用这种技巧。stringr的许多函数,都对stringi和 base R 里的底层函数做了方便易用的封装。

传给str_*()函数的额外实参,会直接透传给那些底层函数。

函数作为返回值

外层函数可以定义一个内层函数(既可以具名,也可以是下面要讲的匿名函数),并把它作为返回值。

被返回的函数会带上它被定义时所处的环境。

times_y <- function(x) {
  # anonymous function - see next section
  \(y) x * y
}

f <- times_y(3)
class(f)
#> [1] "function"

f(4)
#> [1] 12

匿名函数

定义一个函数时,我们通常会把得到的函数对象绑定到一个变量上。

x <- 2

# function definition captures x from the environment
doubleit_short <- function(y) x * y

这样在脚本后面使用这个函数会很方便,但这种绑定并非必需。一个很短、只用一次的函数,在当前的上下文里也可能很有用。不做名称绑定,这样的函数就叫匿名函数。

匿名函数用得如此普遍,以至于(从 R v4.1.0 起)有了一种定义它们的简写语法:把function这个词换成反斜杠\。

上一节讲返回值时,就用了这样一个例子。

修改时复制

R 允许给向量的单个元素赋值。如果把一个向量作为实参传进函数,在返回之前又在函数体里修改它,那么我们得到的就是一个修改过的向量。

但原来的那个向量怎么样了呢?

f <- function(vec) {
  vec[1] <- 42
  vec
}

vals <- c(1, 3, 4)

# f() returns a modified vector
f(vals)
#> [1] 42  3  4  

# the original vector is unchanged.
vals
#> [1] 1 3 4

R 是一门为数据科学设计的语言。收集这些数据可能要花掉大量时间、精力,还可能花费高得令人咋舌的金钱:所以千万不能把它弄坏!

一般的策略(也有少数例外)是修改时复制。如果某个对象(比如向量)的改动有可能在之后引发问题,R 会返回一个修改过的副本,而原来的对象保持不变。

复制大型数据结构在计算上可能很昂贵,但与数据被弄坏相比,这通常是两害相权取其轻。

Caution

小心那些会反复复制同一份数据的操作。

套用(Tidyverse 作者)Hadley Wickham 的说法:

循环本身并不慢,但一不留神就很容易把慢操作放进循环里。

向量化或高阶函数可以帮你避开这类性能杀手。

说明

你的公司一直在使用地图数据来构建网页。 经过一番讨论,你们决定开始使用 R 来动态完成一些计算。

请尽量直接处理向量点:不需要把它们拆分成单独的x和y坐标。

1. 缩放坐标

实现scale(point, s)函数,它接收一个任意维度的点,并按预先定义好的s对它进行缩放。

# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4

2. 平移坐标

实现translate(point, ...)函数,它返回一个新的点,其中每个坐标都按省略号里的值移动。

这个函数必须能处理任意维度的点,并且省略号中参数的数量要与该维度相匹配。

point2d <- c(2, 3)

# supply dx, dy translations
translate(point2d, 0.5, 0.6)
#> [1] 2.5 3.6

point3d <- c(2, 3, 4)

# supply dx, dy, dz translations
translate(point3d, 0.5, 0.6, 0.7)
#> [1] 2.5 3.6 4.7

3. 变换一个 2D 点

你的有些队友对 R 不太熟悉,所以你决定用函数闭包来为{x, y}坐标对创建可复用的变换。

实现transform2d(dx, dy, s)函数,它返回一个函数,利用闭包对点进行可重复的二维平移和缩放。

缩放参数s应该是可选的,默认值为1。

返回的函数应接收一个 2D 点,然后:

  1. 按预先定义好的dx和dy平移它
  2. 按s缩放它
# scale all coordinates by the same amount (2) in this example
# your code needs to be able to handle a vector `s`

tf2d <- transform2d(0.5, 0.6, 2)
class(tf2d)
#> [1] "function"

tf2d(c(2, 3))
#> [1] 5.0 7.2

# Accept default for `s`
tf2d <- transform2d(0.5, 0.6)

tf2d(c(2, 3))
#> 2.5 3.6

运算顺序很重要:先平移再缩放才能得到正确结果,先缩放再平移则不行。 用专业一点的说法,这两个运算是_不可交换的_。

4. 变换一个 3D 点

地图数据也可能包含高度,用来在屏幕上添加阴影和等高线。

实现transform3d(dx, dy, dz, s)函数,它返回一个函数。

缩放参数s应该是可选的,默认值为1。

返回的函数应接收一个 3D 点,按预先定义好的值平移它,然后按s缩放它。

# using default `s`
tf3d <- transform3d(0.5, 0.6, 0.7)
point <- c(2, 3, 4)
tf3d(point)
#> [1] 2.5 3.6 4.7

# using vector `s`
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
tf3d(point)
#> [1] 1.25 2.16 3.29
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
R Exercism

准备好开始 坐标变换 了吗?

注册 Exercism,借助 21 个概念111 个练习 和真人导师指导,学习并掌握 R,全部免费。