12 Basic: control flow

到目前为止,本课程中写过的所有代码都是线性执行的,即按从头到尾的顺序执行所有语句。只使用这种单向控制流,可以编写一些非常简单的程序,这种写法可以视作是不对代码的执行顺序做任何人为的控制。

简单来讲,控制流(control flow)就是控制代码的流程,通过一定方式控制代码执行的顺序。Control flow 能够极大地增加代码的功能性,是现代编程语言的经典成分,绝大多数编程语言中都有控制流的模块。

Control flow 可以分为 conditional flow (条件流)和 loop (循环)两大类。

(本节内所有的流程图都来自GeeksforGeeks

12.1 Conditional flow

12.1.1 if else

基本语法结构

if (cond) {
  cons.expr
} else {
  alt.expr
}

如果cond的结果是TRUE,执行cons.expr,反之则执行alt.exprcond通常都是 relational operator 的结果,或者是is.xxx()类 function 的结果。cond的执行结果要求是长度为 1 的 logical vector,如果长度超过 1,执行时会得到一个 error,详见12.1.1.3

age <- 12
# age <- c(11, 12)
if (age < 12) {
  "elementary school"
} else {
  "middle school"
}
#> [1] "middle school"

在 Rstudio 中可以通过 Snippets 的功能快速输入if else结构:

if else 有以下 4 种情况:

12.1.1.1 Single cond without else

如果不需要else部分,可以简化为:

if (cond) {
  expr
}

exprcond都很简短时,可以进一步简化为:

if (cond) cond expr
if (cond) cons.expr else alt.expr

例如:

input <- 1
if (input == 1) "yes" 
#> [1] "yes"
if (input == 2) "yes" else "no"
#> [1] "no"

12.1.1.2 Multiple conds with else if

if (cond1) {
  cond1.expr
} else if (cond2) {
  cond2.expr
} else if (cond3) {
  cond3.expr
}

例如:

input <- 1
if (input == 1) {
  "a"
} else if (input == 2) {
  "b"
} else if (input == 3) {
  "c"
}
#> [1] "a"

但要注意,上面这段代码,如果input不是1,2,3这 3 个互斥条件中的任意一个,执行时不会有任何的提示,这种情况属于没有考虑到所有的可能,存在漏洞,容易出问题,建议改成以下写法:

input <- 4
if (input == 1) {
  "a"
} else if (input == 2) {
  "b"
} else if (input == 3) {
  "c"
} else {
  stop(paste("Exception encountered,", input, "does not satisfy any condition!"))
}
#> Error: Exception encountered, 4 does not satisfy any condition!

同样,也可以通过 Snippets 快速输入if else if结构:

12.1.1.3 Complex cond

if结构中的cond可以是比较复杂的关系 + 逻辑运算的结果,具体又分为两种情况:

  1. 结果是长度为 1 的 logical vector
a <- 0
if (a > -3 & a < -1 || a > 1 & a < 3) {
  "yes"
}
  1. 结果是长度大于 1 的 logical vector

cond的结果是长度大于 1 的 logical vector,在 4.2.0 以下版本的 R 中会得到一个 warning,例如

rnd <- runif(3)
rnd
if (rnd > 0.95) {
  "you are awfully lucky"
}

运行结果为

[1] 0.5021010 0.8256499 0.1971661
Warning message:
In if (rnd > 0.95) { :
  the condition has length > 1 and only the first element will be used

相同的代码在 4.2.0 及以上版本的 R 中,会得到一个 error,例如

Error in if (rnd > 0.95) { : the condition has length > 1

因此,如果是需要依据expr中所有元素的对比结果来作出最终判断,那么正确的做法是运用all(x)any(x),其中x为一个 logical vector。如果x中所有的值都是TRUE,那么all(x)的结果为TRUE,反之则是FALSE。如果x中所有的值都是FALSEany(x)的结果才是FALSE,反之都是TRUE

rnd <- runif(3)
rnd
#> [1] 0.7188258 0.2188064 0.6252297
if (all(rnd > 0.95)) {
  "you are awfully lucky"
} else if (any(rnd > 0.95)) {
  "you are lucky"
} else {
  "sorry, you are out of luck"
}
#> [1] "sorry, you are out of luck"

12.1.1.4 Vecterized if else: ifelse()

当需要针对一个 vector、matrix、array 或 data.frame 中的所有元素按照是否符合某种条件为依据统一处理时,建议使用ifelse(),是if else的向量化版本,其基本语法结构为:

ifelse(test, yes, no)

其中,test表示对某个输入 object 的所有元素的检验,检验的结果是和该 object 同样大小的一个 logical object ;yes是 object 的元素中检测为TRUE时对应的返回值;no是检测结果为FALSE时的返回值。yesno可以是一行语句,其执行结果必须和输入的 object 同样大小;如果不是,则会触发 recycling rule,(详见7.2.2),直至满足该限制。

例如:

m1 <- matrix(sample(1:100, 16), nrow = 4, ncol = 4)
m1
#>      [,1] [,2] [,3] [,4]
#> [1,]    5   51   29   89
#> [2,]   71   58   98   67
#> [3,]   70   18   84   13
#> [4,]   86   20   69   92
print("检查每个元素是奇数还是偶数")
#> [1] "检查每个元素是奇数还是偶数"
ifelse(m1 %% 2 == 0, "even", "odd")  
#>      [,1]   [,2]   [,3]   [,4]  
#> [1,] "odd"  "odd"  "odd"  "odd" 
#> [2,] "odd"  "even" "even" "odd" 
#> [3,] "even" "even" "even" "odd" 
#> [4,] "even" "even" "odd"  "even"
# 等价于 
# ifelse(
#   m1 %% 2 == 0, 
#   matrix("even", nrow = 4, ncol = 4), 
#   matrix("odd", nrow = 4, ncol = 4)
# )
print("将所有奇数变成偶数,偶数维持不变")
#> [1] "将所有奇数变成偶数,偶数维持不变"
ifelse(m1 %% 2 == 0, m1, m1 + 1)
#>      [,1] [,2] [,3] [,4]
#> [1,]    6   52   30   90
#> [2,]   72   58   98   68
#> [3,]   70   18   84   14
#> [4,]   86   20   70   92

12.1.2 switch (optional)

switch结构可以视作是多个condif结构的等价写法,其基本语法结构如下:

switch (object,
  case1 = expr1,
  case2 = expr2
)

其中,object的不同值,对应不同的case,例如:

input <- 1
switch(input,
  "1" = print("a"),
  "2" = print("b"),
  "3" = print("c"),
  stop("No pre-set output for current `input`")
)
#> [1] "a"

switch结构也可以通过 Snippets 快速键入:

12.2 Loop

Loop 结构包括三种,forwhilerepeat

12.2.1 for

基本语法结构:

for(var in seq) {
  expr
}

例如:

for (i in c("A", "d", "r")) {
  print(i)  # 必须用 print 才能输出
}
#> [1] "A"
#> [1] "d"
#> [1] "r"

其中seq1:3,表示一个长度为 3 的 vector,所有 elements 会根据位置上的先后顺序被 loop,第 1 次 loop 就把seq的第 1 个 element 取出来赋值给var,即i,然后执行expr。依次 loop 下去,直到seq的所有 elements 都被 loop 完毕。

for结构也可以通过 Snippets 快速键入:

for结构有以下使用技巧\注意事项:

12.2.1.1 seq can be any object

# character
df <- data.frame(
  score_last = c(100, 88, 93), 
  score_current = c(99, 96, 77)
)
for (i in names(df)) {
  print(i)
  print(mean(df[, i]))
}
#> [1] "score_last"
#> [1] 93.66667
#> [1] "score_current"
#> [1] 90.66667
# numeric
df <- data.frame(
  score_last = c(100, 88, 93), 
  score_current = c(99, 96, 77)
)
for (i in 1:length(df)) {
  print(i)
  print(mean(df[, i]))
}
#> [1] 1
#> [1] 93.66667
#> [1] 2
#> [1] 90.66667
# numeric
df <- data.frame(
  score_last = c(100, 88, 93), 
  score_current = c(99, 96, 77)
)
for (i in df) {
  print(i)
  print(mean(i))
}
#> [1] 100  88  93
#> [1] 93.66667
#> [1] 99 96 77
#> [1] 90.66667
# numeric
m1 <- matrix(1:16, nrow = 4, ncol = 4)
cumsum_m1 <- 0
for (i in m1) {
  print(i)
  cumsum_m1 <- cumsum_m1 + i
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
#> [1] 6
#> [1] 7
#> [1] 8
#> [1] 9
#> [1] 10
#> [1] 11
#> [1] 12
#> [1] 13
#> [1] 14
#> [1] 15
#> [1] 16
print(cumsum_m1)
#> [1] 136
# numeric
l1 <- list(1:2, "a", list(1, 2, 3))
for (i in l1) {
  print(i)
}
#> [1] 1 2
#> [1] "a"
#> [[1]]
#> [1] 1
#> 
#> [[2]]
#> [1] 2
#> 
#> [[3]]
#> [1] 3

12.2.1.2 seq is a fixed anonymous object within for

seq的本质是一个短暂存在的anonymous object,随着for结构的开始而出现,随着for结构的结束而消亡。这也就意味着,如果使用一个已经存在的 object 作为seq,一旦for结构开始执行,所使用的seq就固定了,在for结构的内部改动该 object 不会改变seq,因为二者是两个完全不同的 objects。

vec_num <- 1:5
for (i in vec_num) {
  vec_num <- 100
  cat("i =", i, "vec_num =", vec_num, "\n")
}
#> i = 1 vec_num = 100 
#> i = 2 vec_num = 100 
#> i = 3 vec_num = 100 
#> i = 4 vec_num = 100 
#> i = 5 vec_num = 100

12.2.1.3 Use seq_along()

使用seq_along()而非:生成for结构中的seq

seq_along(x)产生一个和x等长的自然数序列,起点为 1,终点为x的长度。

vec <- runif(3)
seq_along(vec)
#> [1] 1 2 3
vec <- NULL
seq_along(vec)
#> integer(0)

for结构的常见用途是重复某个操作一定次数,并且将这些重复操作所得结果储存,例如

means <- c(80, 90, 100)
out_colon <- vector("list", length(means))
for (i in 1:length(means)) {
  out_colon[[i]] <- rnorm(10, means[[i]])
}
out_colon
#> [[1]]
#>  [1] 79.76980 77.61132 79.90355 79.77655 80.94621 79.02797
#>  [7] 80.35437 79.52474 80.77968 80.44533
#> 
#> [[2]]
#>  [1] 90.72544 91.42940 91.37350 89.07315 89.44835 88.24212
#>  [7] 90.43240 88.94611 89.92620 90.58424
#> 
#> [[3]]
#>  [1]  98.83501 101.51207  99.76628 101.71629 100.53997
#>  [6] 100.81183  98.51398 100.10704 101.15548  99.79305

length(x)中的x长度大于 0 时,使用1:length(x)和使用seq_along()效果一样,

means <- c(80, 90, 100)
out_seqalong <- vector("list", length(means))
for (i in seq_along(means)) {
  out_seqalong[[i]] <- rnorm(10, means[[i]])
}
out_seqalong
#> [[1]]
#>  [1] 81.05216 81.41232 79.41046 80.22320 78.22984 79.72294
#>  [7] 82.85554 80.54777 79.61770 81.08065
#> 
#> [[2]]
#>  [1] 92.41319 90.33130 89.74081 90.68757 89.33327 90.71700
#>  [7] 90.29722 90.47815 90.86888 90.71255
#> 
#> [[3]]
#>  [1]  98.06717 100.32467  97.89801  98.65786 100.50766
#>  [6]  99.52305  99.67196 101.20876 100.53723 100.99713

但当length(x)中的x长度为 0 时,使用1:length(x)会有问题,

means <- c()
out_seqalong <- vector("list", length(means))
for (i in 1:length(means)) {
  out_colon[[i]] <- rnorm(10, means[[i]])
}
#> Error in rnorm(10, means[[i]]): invalid arguments
out_colon
#> [[1]]
#>  [1] 79.76980 77.61132 79.90355 79.77655 80.94621 79.02797
#>  [7] 80.35437 79.52474 80.77968 80.44533
#> 
#> [[2]]
#>  [1] 90.72544 91.42940 91.37350 89.07315 89.44835 88.24212
#>  [7] 90.43240 88.94611 89.92620 90.58424
#> 
#> [[3]]
#>  [1]  98.83501 101.51207  99.76628 101.71629 100.53997
#>  [6] 100.81183  98.51398 100.10704 101.15548  99.79305

原因是在于:既可以生成升序自然数序列,又可以生成降序自然数序列,

length(c())
#> [1] 0
1:length(c())  # equivalent to 1:0
#> [1] 1 0

但显然,当想要循环的对象长度为 0 时,应该是不循环才更符合逻辑,所以使用seq_along()会更符合预期,

means <- c()
out_seqalong <- vector("list", length(means))
seq_along(means)
#> integer(0)
for (i in seq_along(means)) {
  out_seqalong[[i]] <- rnorm(10, means[[i]])
}
out_seqalong
#> list()

12.2.1.4 var is a named object

seq不同,var不是一个 anonymous object,它随着for结构的开始而出现,但不会随着for结构的结束而消亡,所以有两个特点:

  • var在当次 loop 内可以更改,但是不会影响下次 loop。
for (i in 1:5) {
  cat("the var used in the current loop is", i, "\n")
  i <- i + 5
  cat("the var now has been changed to", i, "\n")
}
#> the var used in the current loop is 1 
#> the var now has been changed to 6 
#> the var used in the current loop is 2 
#> the var now has been changed to 7 
#> the var used in the current loop is 3 
#> the var now has been changed to 8 
#> the var used in the current loop is 4 
#> the var now has been changed to 9 
#> the var used in the current loop is 5 
#> the var now has been changed to 10
  • varfor结构执行完毕后会储存在该for结构所在的 environment 里,其 value 为 seq的最后一个 element。
for (i in 1:5) {
}
i
#> [1] 5

12.2.1.5 Use var as subscript

使用var作为位置下标(subscript),用作 subsetting 的 index,可以实现遍历的效果。

n_ob <- 1000
medians_col <- vector(mode = "integer", length = 3)
df <- data.frame(
  c1 = sample(10000, size = n_ob),
  c2 = sample(10000, size = n_ob),
  c3 = sample(10000, size = n_ob)
)
for (r in 1:3) {
  medians_col[r] <- median(df[, r])
}
medians_col
#> [1] 5283 5370 4929

但凡是采用这个使用技巧时,如果for结构里发现有语句是通过subcript取子集,但又没使用该for结构中的var作为下标,很有可能就是出错了。

例 1:

rm(list=ls())
set.seed(1)
J <- 1000
I <- 30
K <- 30
D <- 1.7
X <- matrix(NA, J, I)
P <- matrix(NA, J, I)

theta<-rnorm(J, 0, 1)
b<-rnorm(I, 0, 1)
theta[theta>3] <- 3
theta[theta<-3] <- -3
b[b > 3] <- 3
b[b < -3] <- -3

for(j in 1:J){
  for(i in 1:I){
    P[j, i] <- 1/(1 + exp(-D*(theta[j] - b[i])))
    r <- runif(1, 0, 1)
    if(P[j,i] < r){
      X[j, i] <- 0
    }else{
      X[j, i] <- 1
    }
  }
}
theta_k<-seq(-3, 3, length.out = K)
theta_end<-matrix(NA, J, 1)
L_k<-matrix(NA, K, 1)

for(j in 1:J){
  for(k in 1:K){
    p_k <- 1/(1 + exp(-D*(theta_k[k] - b)))
    for(i in 1:I){
      p_k[i] <- ifelse(
        X[j,i] == 0,
        1-p_k[i],
        p_k[i]
      )
      L_k[k] <- prod(p_k)
    }
    fenzi <- sum(theta_k*L_k*((1/sqrt(2*pi))*exp(-(theta_k)^2/2)))
    fenmu <- sum(L_k*((1/sqrt(2*pi))*exp(-(theta_k)^2/2)))
  }
  theta_end[j] <- fenzi/fenmu
}
print(mean(abs(theta_end - theta)))

例 2:

# Hello everyone : )
# 
# 
# 
# I was trying to write a function for evaluating the p-values of the t-test of a lm model, I know is a little bit silly and probably useless but I want to practice.
# 
# The issue here is that it only evaluates the first variable.
# 
# Here is the code:

#Data
library(ISLR2)

Auto = tibble(Auto)

#Model

lm.fit = lm(mpg ~ horsepower, data = Auto)

#Function for evaluate p-values

tStest = function(x) {
  x = as.numeric(x)
  a = rep(0,length(x))
  for(i in seq_along(x)) {
    if (x[i] > 0.025) {
      a[i] = 'Accept Ho'
    } else {
      a = 'Reject Ho'
    }
  }
  print(a)
}

pv = summary(lm.fit)$coefficients[, 4] #p-values

tStest(pv) #only returns one value

#But it works with a simple vector

v = c(1,2,3)

tStest(v)

# Does anyone know where is the problem? Also I'm interested in other approaches to achieve the same objective
# 
# Sorry about my broken english, and thank you in advance

12.2.1.6 Initialize output object

输出结果(for结构中每次 loop 改动的结果变量)需提前初始化。

在前面的例子中,medians_col就是for结构的输出结果,medians_col <- vector(mode = "integer", length = 3)就是初始化,即在for结构开始前给medians_col赋值,并且大小也根据预期提前设定好,这样for结构可以顺利执行,并且执行效率最高。反之,如果不提前初始化结果变量,那么在for结构执行时,global environment 里是没有medians_col的,报错并终止:

rm(medians_col)
n_ob <- 1000
# medians_col <- vector(mode = "integer", length = 3)
df <- data.frame(
  c1 = sample(10000, size = n_ob),
  c2 = sample(10000, size = n_ob),
  c3 = sample(10000, size = n_ob)
)
for (r in 1:3) {
  medians_col[r] <- median(df[, r])
}
#> Error: object 'medians_col' not found

12.2.1.7 Alter the process of loop structure

通常在 loop 结构中,会使用if结构来实现满足条件时改动 loop 结构的执行过程。有 2 种改动,nextbreak

  • next:跳过当前 loop
for (i in 1:3) {
  if (i == 2) next
  print(i)
}
#> [1] 1
#> [1] 3
  • break:跳出当前 loop 结构
for (i in 1:3) {
  if (i == 3) break
  print(i)
}
#> [1] 1
#> [1] 2

nextbreak适用于所有三种 loop 结构。

12.2.1.9 The best scenario to use for

for结构最适合的任务情境应当是前一次 loop 和后一次 loop 有依赖关系,这种情况只有按顺序执行的 loop 结构才能够处理。不同次 loop 之间没有任何联系彼此独立时,都可以有替代for结构的写法。

Loops 间存在联系的示例:

num_ite <- 20
a <- rep(0, num_ite)
for (i in 1:num_ite) {
  if (i != 1) {
    a[i] <- a[i - 1] + i
  }
}
a
#>  [1]   0   2   5   9  14  20  27  35  44  54  65  77  90 104
#> [15] 119 135 152 170 189 209

12.2.2 while

while结构可以视作是for+if

基本语法结构:

while(cond) {
  expr
}

while结构执行前,当cond的执行结果为TRUE时,才会进入while结构,否则会直接跳过。进入while结构后,执行expr,然后会检查cond的执行结果是否为TRUE,是则继续下一次 loop,否则跳出while结构。

在使用while结构的时候要注意,一定要确保在expr中,cond会被更改,并酌情设定退出机制,否则非常容易陷入 forever loop。

# forever loop
i <- 1
while (i <= 5) {
  print(i)
}
# make sure:
#   1. cond is modified within each loop
#   2. loop can always be jumped out of
set.seed(123)
rnd_unif <- runif(1, -1, 1)
cum_sum_rnd_unif <- 0
count <- 1
while (cum_sum_rnd_unif <= 1) {
  cum_sum_rnd_unif <- cum_sum_rnd_unif + rnd_unif
  print(cum_sum_rnd_unif)
  count <- count + 1
  if (count > 20) break
}
#> [1] -0.424845
#> [1] -0.8496899
#> [1] -1.274535
#> [1] -1.69938
#> [1] -2.124225
#> [1] -2.54907
#> [1] -2.973915
#> [1] -3.39876
#> [1] -3.823605
#> [1] -4.24845
#> [1] -4.673295
#> [1] -5.09814
#> [1] -5.522984
#> [1] -5.947829
#> [1] -6.372674
#> [1] -6.797519
#> [1] -7.222364
#> [1] -7.647209
#> [1] -8.072054
#> [1] -8.496899
print(count)
#> [1] 21

while结构也可以通过 Snippets 快速键入:

12.2.3 repeat

repeat结构同样也可以视作是for+if

基本语法结构:

repeat {
  expr
}

在使用repeat结构的时候要注意,一定要确保expr中有带breakif结构,保证不会陷入 forever loop:

i <- 1
repeat {
  print(i)
  i <- i + 1
  if (i > 5) {
    break
  }
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5

考虑到whilerepeat结构在不够谨慎的情况下都有陷入 forever loop 的风险,而二者的本质作用都是执行多次 loop,和for结构并无差异,因此,在绝大多数情况下都建议使用for结构。

例如前文展示在使用while结构时避免 forever loop 的代码完全可以用如下for结构替代:

set.seed(123)
rnd_unif <- runif(1, min = -1, max = 1)
cum_sum_rnd_unif <- 0
n_loop <- 20
for (i in 1:n_loop) {
  cum_sum_rnd_unif <- cum_sum_rnd_unif + rnd_unif
  print(cum_sum_rnd_unif)
}
#> [1] -0.424845
#> [1] -0.8496899
#> [1] -1.274535
#> [1] -1.69938
#> [1] -2.124225
#> [1] -2.54907
#> [1] -2.973915
#> [1] -3.39876
#> [1] -3.823605
#> [1] -4.24845
#> [1] -4.673295
#> [1] -5.09814
#> [1] -5.522984
#> [1] -5.947829
#> [1] -6.372674
#> [1] -6.797519
#> [1] -7.222364
#> [1] -7.647209
#> [1] -8.072054
#> [1] -8.496899

12.3 Recap

  1. 不论是使用if结构还是switch结构,在构建包含多个互斥conds 的结构时都需要将所有的情况都考虑到;
  2. if结构中,cond为 logical vector 时,根据需要运用all()(相当于&)和any()(相当于|);
  3. ifelse()适用于快速检验 vector、matrix、array、data.frame 中的所有 elements,是if else的向量化写法;
  4. for结构中,输出结果需初始化;
  5. loop 结构中,next跳过当前 loop,break跳出整个 loop 结构;
  6. 必须要使用 loop 结构时,尽量使用for结构。