#Anything after a `#' is a `comment' and ignored by R
# - use them to write notes to yourself in your scripts
empty.vector <- logical(0) # 1
numbers <- c(1, 3, 5, 3) # 2
vector.maths <- numbers * 4.5 # 3Appendix D — A technical introduction to R
Forward—read this before reading any more
What follows is the first two sections of my semester-long class on programming in R. This material is ususally covered in two weeks, containing two lectures and four practical sessions. Thus there is a very good chance you will find what follows terrifyingly in-depth, but if you find it even mildly interesting you may enjoy taking my class. I have removed the exercises, for fear that you might try and attempt them.
D.1 Fundamentals
Overview
This is the first of four sessions in the ‘Introduction to R’ series. Over these sessions I am going to teach you the fundamentals of programming, using the language R as a guide. The fundamentals you learn in the next few sessions will recur throughout this course: if you can spot those recurrences, you’ll get more out of the course. In this session we’re going to learn the basics of R: the differences between a vector, matrix, and a list, subsetting, and basic plotting. This is the only session where I make heavy use of footnotes; I footnote material that I think is useful to know but potentially confusing when you’re inexperienced. Learning a programming language is like learning a real language: if you’re paying attention, the most commonly-used parts of a language are actually the most confusing. I’ve flagged important, but potentially confusing, things here for you to refer back to when you’re feeling a bit more settled: it’s possible to be conversant in French without understanding every grammatical rule, and the same is true of R 1.
I hate to ask this, but please forget everything you may have been taught about R in the past. The terms I am going to use to describe things (e.g., atomics, coercion, and scope) often sound a bit scary, and so are rarely used in introductory courses. These are the correct technical terms for concepts that many programming languages share, and are the terms used by the writers of R—trying to map these onto the simplified versions of concepts you have been taught in the past can make your life more difficult. Learning a new language requires some sweat; resist the effort to ask for help the moment you encounter a problem. Calmly read any error message you encounter and see if you can figure out what it might be trying to say. Please do ask me anything you want, but my first question will almost certainly be “what is the error message” and my second question will therefore be “and what do you think it means”… Not knowing the answer to the first question will make you look rather foolish, but the second question is the hardest and is something I am happy to help you with. Remember computers are very literal: something like Error: object 'result' not found often means you typed ‘result’, not ‘Result’. Finally, a general piece of advice: the bonus exercises and information at the end of each session in this section are, of course, not mandatory. That said, many of them will be useful to you at points in the future—if you didn’t have time to complete them during the course, you may find going back over them in your spare time afterwards useful. Bonus exercise(s) that are successfully completed will result in bonus marks.
D.1.1 Atoms
Everything in R is an object, and the basic building blocks of objects are atomics. There are many types of atomic; the common ones are logical (TRUE/FALSE), integer (whole numbers like \(1\)), double (floating point numbers like \(1.0\) and \(1.1\)), complex numbers (imaginary numbers; \(i\)), character (strings of letters “like this”), and raw (bytes; things computers read). We treat integer and double as numeric and you can essentially just pretend there’s an atomic called numeric and forget about the distinction2. All atomics are vectors: this means you can have more than one numeric value inside a single atomic variable (like vectors in maths). However, not all vectors are atomics: the simplest example of this is factors, which are described in ‘Compound data types’ below. We can concatenate vectors together to make longer vectors; thus two vectors of length two can be combined to make a single vector of length four.
- Can be read out loud as “empty dot vector gets (a) logical (vector of length) 0”.
<-is the assignment operator (“gets”); an operator is a thing like+or*which takes an object and does something with it 3. In this case it’s taking the output from the expression (a set of instructions to be executed by the computer)logical(0)and storing its output in a new variable calledempty.vector. (2) shows theconcatenate function, which joins vectors together into a single vector. In this case, it takes four vectors, each of which is of length 1, and makes a single vector of length 4. Once again, vectors in programming are like vectors in math: they have lengths, which means there can be more than one number (or character, or logical, or…) inside a single vector. (3) shows how you can do mathematical operations across an entire vector at the same time (which is useful). As with all the examples I’m going to give you in this course, type them into your computer yourself (copy-pasting won’t build your muscle memory for the language) and examine their results by typing things likenumbersinto the console to see what the code does. I will say it again: typing, not copy-pasting, my examples is one of your assignments, and doing so will give you experience of what error messages occur when you mis-type, as well as a deeper understanding of the language. You will learn faster if you type. Trust me.
One useful feature of vectors is subsetting. The best way to understand subsetting, which is sometimes called slicing, is to see it in action. To demonstrate this, I’m going to use a built-in vector called letters—this variable (an object in R, like a vector, that contains a value) is available whenever you start R. Type each of the following lines into R and see what you get, then I’ll go through what’s going on in each line.
letters # 1
#> [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
#> [20] "t" "u" "v" "w" "x" "y" "z"
1:5 # 2
#> [1] 1 2 3 4 5
letters[1:5] # 3
#> [1] "a" "b" "c" "d" "e"
x <- 1:26 # 4
x < 10 # 5
#> [1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE FALSE FALSE FALSE
#> [13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> [25] FALSE FALSE
letters[x < 10] # 6
#> [1] "a" "b" "c" "d" "e" "f" "g" "h" "i"
names(x) <- letters # 7
x # 8
#> a b c d e f g h i j k l m n o p q r s t u v w x y z
#> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
x["a"] # 9
#> a
#> 1- shows the contents of
letters—all the letters! (2) shows a shortcut for generating anumericvectorwith all the numbers between 1 and 5 using the:operator. (3) shows slicing, which uses the[and]operators. It means “take the vector inside the[]and give me those elements ofletters”. In this case, that’s the first, second, …, fifth elements—the letters a, b, …, e. (4) makes a new vector, and (5) shows us alogicalvector created using that vector and the<operator (>,<=,==4, and!=do what you would expect too). Play around with different operators to get a feel for how they work: you will need to understand these when we doifstatements in the next lectures. In (6), we see that we can also use alogicalvector for subsetting [unlike the numeric in (3)]. (7) assigns names to a vector, whose effect is obvious in (8). Finally, (9) shows we can also use acharactervector to subset a named vector.
D.1.2 Multiple dimensions and advanced subsetting
Vectors are uni-dimensional, but a matrix has two dimensions and an array can have as many as you want5. You can subset each dimension of these data-types separately, using the , operator to separate them. These variables must all be of the same atomic type, however. When working with multiple dimensions, R expects matrix[row,column], and leaving a dimension blank gives you everything along that dimension. It’s row,col, so remember: in a healthy relationship, a row is followed by a cuddle; if having a cuddle leads to a row, something’s going wrong. “Row” is the British word for “argument”; it took me years to come up with this mnemonic and I’m afraid that’s the best I can do so I’m not changing it for the US. See if you can figure out how these data types work by running the examples below; notice how I subset on the output of array without even bothering to save an intermediate variable in the last line.
my.mat <- matrix(1:25, nrow=5, ncol=5)
my.mat
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 1 6 11 16 21
#> [2,] 2 7 12 17 22
#> [3,] 3 8 13 18 23
#> [4,] 4 9 14 19 24
#> [5,] 5 10 15 20 25
my.mat[1:5,]
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 1 6 11 16 21
#> [2,] 2 7 12 17 22
#> [3,] 3 8 13 18 23
#> [4,] 4 9 14 19 24
#> [5,] 5 10 15 20 25
my.mat[,1:5]
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 1 6 11 16 21
#> [2,] 2 7 12 17 22
#> [3,] 3 8 13 18 23
#> [4,] 4 9 14 19 24
#> [5,] 5 10 15 20 25
my.mat[1,3]
#> [1] 11
my.mat[-1,]
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 2 7 12 17 22
#> [2,] 3 8 13 18 23
#> [3,] 4 9 14 19 24
#> [4,] 5 10 15 20 25
my.mat[,-1:-2]
#> [,1] [,2] [,3]
#> [1,] 11 16 21
#> [2,] 12 17 22
#> [3,] 13 18 23
#> [4,] 14 19 24
#> [5,] 15 20 25
#...negative numbers exclude elements...
my.mat[1:2,1:2]
#> [,1] [,2]
#> [1,] 1 6
#> [2,] 2 7
array(1:8, dim=c(2,2,2))[,,1]
#> [,1] [,2]
#> [1,] 1 3
#> [2,] 2 4While we’re here, let me mention my two favorite things to use in R: %in% and match. %in% is an operator6 that tells you whether the elements of the vector on its left are in the vector on its right. match tells you where elements in a vector are found in another vector. By default, match returns NA if there is no match. NA is a special R value that flags what I think of as ‘logic errors’; if you ask where something is in a vector and it’s not there, you get NA (it couldn’t return anything else as that might be interpreted as a location). So, pop quiz: what happens when you run NA + 3? NULL is a value you will encounter occasionally as well, and it means ‘nothing’ (“null and void”). It’s also often used to flag problems as well, but put simply it could just be described as a thing that indicates the absence of a thing (R is sometimes very Zen). R also has Inf, whose meaning is obvious if you divide by zero. Run these lines to understand what’s going on.
characters <- c("a", "f", "3")
#...Note that "3" is a character, and 3 is a number...
characters %in% letters
#> [1] TRUE TRUE FALSE
match(characters, letters)
#> [1] 1 6 NA
letters[match(characters, letters)]
#> [1] "a" "f" NA
characters[!characters %in% letters]
#> [1] "3"
#...a '!' negates a logical...
"a" == 'a' #" and ' are essentially interchangeable
#> [1] TRUED.1.3 Compound data types
There are many kinds of vector, but the most important non-atomic type is the list. A list is special: its elements need not be of the same type. This means you can have a list made up of a numeric, character, and a logical vector. Like all vectors, a list can be named, which allows you to pull out individual parts of a list (see also subsetting, below). To do that, you need to use the $ operator, as below. You can also subset a list, as you can any vector; the convention is to use [[]] when pulling out a single element of a list, however.
my.list <- list(n=c(1,3,5), c=c("hello","world"), l=c(TRUE,FALSE,TRUE))
my.list$n
#> [1] 1 3 5
my.list$c
#> [1] "hello" "world"
my.list[[3]]
#> [1] TRUE FALSE TRUE
my.list[1:2]
#> $n
#> [1] 1 3 5
#>
#> $c
#> [1] "hello" "world"A data.frame is a special kind of list that you will use a lot in your statistical work (but not so much in programming). It lets you use the named features of a list, but also the subsetting features of a matrix (see subsetting below).
my.df <- data.frame(n=c(1,3,5), c=c("hello","big","world"), l=c(TRUE,FALSE,TRUE))
my.df$n
#> [1] 1 3 5
my.df$c
#> [1] "hello" "big" "world"
my.df[1,]
#> n c l
#> 1 1 hello TRUE
my.df[,2:3]
#> c l
#> 1 hello TRUE
#> 2 big FALSE
#> 3 world TRUEThe most dangerous compound data type is a factor. Most people think factors are atomics, and they’re dangerously wrong. A factor is a special data-type that makes statistical analysis of categorical data easier and more efficient to work with, but looks like a character vector. It contains a vector of possible character states (levels) and an internal data vector which stores what number level an element of the vector represents. This matters when people coerce vectors of different types into one-another; coercion turns a vector of one type (e.g., character) into another (e.g., numeric). In factors the data vector is stored as a number, and this is what is coerced, often with dangerously counter-intuitive results. Below I show how to do this (and check the type of an object), and also show the solution to the factor problem. Try them for yourself: factors are very confusing to explain, but if you examine the variable dangerous below (and its levels, and what happens when you coerce it) you’ll figure out what’s going on. If you’re ever going to do statistics in R, keep playing around with this until it makes sense because if you don’t understand this you will make a fatal mistake in the future!
is.numeric(1:5)
#> [1] TRUE
as.numeric(c("1", "3", "5"))
#> [1] 1 3 5
as.numeric(c("one", "three", "five"))
#> Warning: NAs introduced by coercion
#> [1] NA NA NA
dangerous <- factor(c(3, 1, 5))
levels(dangerous)
#> [1] "1" "3" "5"
as.numeric(dangerous) #!!!!!!!!
#> [1] 2 1 3
as.numeric(as.character(dangerous)) #fine
#> [1] 3 1 5D.1.4 Functions
You have already been using functions, and will soon be writing your own. For now, it suffices to say that every time you have written ( or ) you have been calling a function. For example, is.numeric(1:5) calls the function is.numeric, and gives it the object 1:5 as its only argument. That function then returns TRUE. When a function returns, it gives us something back, and a function can only give us one thing back7. In R, what happens in a function stays in a function: it cannot alter the variables we give it as arguments. “Calling” a function might sound a bit strange: imagine you’re yelling for the function to come over, to listen to your instructions (the arguments), carry out the work (execute its code), and then give you back (return) its results.
Arguments are ways of us telling the function how to operate. For example, calling sort(c(1,3,5,1)) is different from calling sort(c(1,3,5,1), decreasing=TRUE). By default, sort sets decreasing as FALSE, but we can change that by altering the argument by name (as we did above) or by making use of the order of the arguments (e.g., sort(c(1,3,5,1),TRUE)). It’s generally better to name your arguments, as there’s no way to know that decreasing is the second argument other than by looking the function up in the help files (more later). If this seems confusing, don’t panic, because we’ll cover it in detail in the next session, but try to remember the gist.
D.1.5 Everyday basics
This is a course in programming, not statistical analysis in R. That said, it is useful to know about the sorts of things that users spend most of their days doing in R.
The read family of functions (e.g., read.csv, read.table) take in spreadsheets (in various formats, hence the suffixes), and return data.frame objects. The write family of functions (there is one for every read function) will output spreadsheets in different formats. These are what we will come to call generic functions later in the course, and as such there are different members of this family for different kinds of data like phylogenies, DNA alignments, spatial grids, etc. The matriarchs of these families are the write and scan functions, which provide more control over the input and output of data, but are best avoided when you’re learning.
You will spend a lot of time plotting things, and most functions related to plotting will let you either supply two variables to plot or a single formula object. This sounds complicated, but it means you’ll either type plot(x, y) or plot(y ~ x). The formula type is useful, and the best one to use, because you’ll need it to run statistical models (lm(y ~ x)). Plotting functions have a common set of graphical paramaters, which are all described in ?par (see the help section below). The list of options is long, but it’s worth taking a quick skim through them—don’t try and memorize them all, but know that these options are there for when you need them. The image function lets you plot matrices; its friend contour can also be fun.
You can load additional libraries with new functions and features by typing something like library(pez)8—but you must first have installed that library either from the menu or using the function install.packages.
D.1.6 Getting help
Every function you will use in R has a help file associated with it. You can look up a known function by running ?name.of.function, or search for a term by running ??search.term. Use quotes if you want to use spaces or are searching for an operator, e.g. ?"%in%" and ??"phylogeny AND ecology". Google is your friend: typing what you are trying to do, or the error message you have found, along with helpful keywords (e.g., R or R ecology) will often reveal the answer. Don’t blindly trust that the first person you read on a mailing list knows the best answer, however, and take things like a user’s rating on StackOverflow or whether the response is from a known R developer into account. Finally, vignettes are excellent story-like tutorial documents that most packages contain; running vignette(package="name.of.package") will tell you the vignettes available for a package, which you can open using the same function.
R help files have a standard format, and as such if you learn the patterns and components in a help file you’re going to have an easier time finding out what a function does. Error messages that you can’t explain are a common symptom of “I didn’t read the help file”-itus… Help files contain:
- Title & Details—Basic overview of what something does, often as you would explain to a layman.
- Usage—Not what it sounds like; these show the arguments (see below) and are incredibly easy to use once you know how to write a function (see the next session).
- Arguments—Detailed descriptions of what each argument changes in a function, and what values they can take.
- Details—More detailed information about either the underlying science of a function (e.g., “what is a regression?” or details of how the function was written where they affect the user.
- Note—Warnings of things that could catch you out
- Value—What the function returns
- See Also—Other functions you might like to use with this function, or possibly the actual function you were looking for…
- Examples—Often detailed, annotated examples of how to use code.
D.2 Control flow and functions
Overview
This is the second of four sessions in the ‘Introduction to R’ series. Today we’re going to focus on the fundamental building blocks of R: control flow and functions. Control flow means “loops”, which some of you may have already heard of, and while you’ve been using functions a lot already we’re now going to learn how to write them. These are the fundamental building blocks of programming: without them, frankly, you can’t program at all.
D.2.1 Control flow
Controlling the flow of program execution is, essentially, all a programmer does. Most programs need to check whether some input data match some given criteria, and execute code if they do. This is carried out using an if statement:
value <- 5
if(value <= 5){
print("Good news!")
}
#> [1] "Good news!"Try running the above code a few times, changing the value of value and seeing what happens. There are two new things above: the if statement itself, and the { } brackets. An if statement looks a bit like a function, and requires that you give it one expression (bit of code) that returns either TRUE or FALSE: a logical vector longer than one, an NA, or anything else will raise an error. So you can use something like is.numeric, identical, or most operators (e.g., ==) in there. The { } indicate what is called a block. A block is a group of lines of code (expressions, technically) that all get executed in a group. Giving if a block like this allows us to group more than one line together to be executed if we want. You will use blocks a lot in R.
Negating something (e.g., if(!is.numeric(x))) is a useful trick, but sometimes you want to do one thing if something is true, and another thing if it isn’t. else lets you do this:
value <- 5
if(value < 5){
print("Less than five!")
} else {
print("Greater than or equal to five!")
}
#> [1] "Greater than or equal to five!"Again, play around with the example above to get a feel for what else does. You cannot use an else on its own. An else to come immediately after whatever block could be executed by the if statement, as R has no way of knowing that it should wait longer for you to give it an else. People often forget to put the else on the same line as the end of the first block—don’t be one of those people!
You don’t always have to use brackets to create a block; just one line of code that can be executed is, very technically, a block, and so you can often get away with writing things a bit more simply like this:
value <- 7
if(value > 5)
print("Greater than five!")
#> [1] "Greater than five!"I don’t advise doing this today, but in the future you will end up doing it because it saves time and space and makes your code a bit quicker to read. Remember to indent you code, as I have been doing, to show where the block is: R doesn’t need it, but humans’ eyes do and it will help you in the future. Always indent blocks for readability. You can also get an else to work without brackets if you follow the rules I gave above, but I wouldn’t advise doing so because you will end up making a very nasty mistake at some point and it’s confusing for others to read.
D.2.2 Loops
Computers are meant to automate repetitive tasks, and so we often want a computer to do something repeatedly for us until we are satisfied with the result. Consider the following:
value <- 0
while(value <= 10){
value <- value + 1
}
print("Finished!")
#> [1] "Finished!"This is our first while loop. This loop continuously executes a block with a single instruction until the condition (value <= 10) is no longer satisfied. Each time the block in a loop is run, we call it an iteration of the loop. These kinds of loops are quite dangerous, because it’s easy to get the computer stuck in an infinite loop because the condition to terminate (finish) the loop is never met. If that seems confusing, consider (but do not run!) the following piece of code:
value <- 0
while(value <= 10){
value <- value - 1
}
print("Finished!")Will this code ever finish running? No, because value will never be greater than 10. If you ever start an infinite loop, click the red cross on the console and/or hit the “escape” key repeatedly. If you’re doing something quite computationally intensive, that often won’t work and you will have to restart R…
Because of this tendency to go on forever, most programmers prefer to use for loops wherever possible:
for(i in 1:10){
print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
#> [1] 6
#> [1] 7
#> [1] 8
#> [1] 9
#> [1] 10The in operator can look a little strange, but essentially the above means “take this block and run it for each element in 1:10, starting with the first one”. i is the traditional loop index to use, and if you write a loop within a loop (which is slow, but you will end up doing at least once) j, then k is preferred for those nested loops. Use those letters: it means you will be able to read other people’s code more easily, and they yours.
It is common (and often best practice) in loops to use loop indices and not to loop directly over the vector you might be interested in. So, for example, while the following is fine:
for(each in letters){
print(each)
}
#> [1] "a"
#> [1] "b"
#> [1] "c"
#> [1] "d"
#> [1] "e"
#> [1] "f"
#> [1] "g"
#> [1] "h"
#> [1] "i"
#> [1] "j"
#> [1] "k"
#> [1] "l"
#> [1] "m"
#> [1] "n"
#> [1] "o"
#> [1] "p"
#> [1] "q"
#> [1] "r"
#> [1] "s"
#> [1] "t"
#> [1] "u"
#> [1] "v"
#> [1] "w"
#> [1] "x"
#> [1] "y"
#> [1] "z"…it’s mostly preferred to write:
for(i in 1:length(letters)){
print(letters[i])
}
#> [1] "a"
#> [1] "b"
#> [1] "c"
#> [1] "d"
#> [1] "e"
#> [1] "f"
#> [1] "g"
#> [1] "h"
#> [1] "i"
#> [1] "j"
#> [1] "k"
#> [1] "l"
#> [1] "m"
#> [1] "n"
#> [1] "o"
#> [1] "p"
#> [1] "q"
#> [1] "r"
#> [1] "s"
#> [1] "t"
#> [1] "u"
#> [1] "v"
#> [1] "w"
#> [1] "x"
#> [1] "y"
#> [1] "z"Note that the length function returns the length of a vector (how long it is), which in the case of letters is 26 as we saw in the previous session. This might seem strange, but otherwise it’s impossible to modify anything in the vector you’re looping over. After a while, you will become so used to seeing a loop index that it’ll be strange not to see it. Compare the following loops, and you’ll see the problem with modifying if you don’t have a loop index:
unchanged <- changed <- c("a", "c", "e")
for(each in changed){
each <- toupper(each)
}
identical(changed, unchanged) # Nothing has happened
#> [1] TRUE
for(i in 1:length(changed)){
changed[i] <- toupper(changed[i])
}
identical(changed, unchanged) #Success! We did something
#> [1] FALSENote how I use the function identical to test whether two vectors are the same (i.e., whether we’ve been successful in doing some work in our loop). See if you can figure out what toupper does for yourself.
D.2.3 Advanced looping
Sometimes you need to break out of a loop altogether—perhaps because you’ve achieved the result you were looking for. Consider the following:
value <- 0 # 1
max.iter <- 1000 # 2
goal <- 2 # 3
for(i in 1:max.iter){ # 4
value <- rnorm(1) # 5
if(value > goal){ # 6
break # 7
} # 8
} # 9
if(i == max.iter){ # 10
stop("Max iteration reached!") # 11
} # 12In the code above, we are trying to draw a random number from a distribution centered at zero (the default), and we want to stop once we get a number above 2. A strange thing to want to do, but bear with me. Since we don’t know when we’ll get a number greater than 2, we keep drawing (at most 1000 times) until we find a number greater than 2. if we find a number greater than 2, we break out of the loop—we stop the loop continuing. R is really quite clever, and so when it sees the keyword break and it’s inside a loop, it knows to break out of whatever loop it’s in and carry on with the rest of the script. Play around with the bits where I define the goal (3) to see what happens when you set it to something that’s very unlikely to come up in a standard Normal distribution (what are the defaults of rnorm again?).
What’s interesting about the above example is how I’ve used break to set a maximum number of iterations. The loop will only proceed for however many iterations are defined on line 2; if that many occurs then by the time we get to line 10 i will be equal to max.iter and we will have hit our limit, so the program spits out an error (which we create with stop; 11). So here you can see how I’ve used a for loop to do something that feels a bit like a while loop (keep looking until you find something), but done it safely with a maximum number of iterations to avoid an infinite loop. Of course, if we found the right value on the last iteration of the loop, then my code would spit out a false error. How would you fix this (and, in the process, make everything much simpler)?
The final loop-related keyword, next, allows you to skip one iteration of a loop and move straight on to the next one. The following, rather contrived, script that will only print random numbers if they’re above a certain threshold gives an example:
value <- 0
threshold <- 1
for(i in 1:10){
value <- rnorm(1)
if(value <= threshold){
next
}
print(value)
}
#> [1] 1.038376
#> [1] 1.237387
#> [1] 1.059047Finally, a piece of advice that you can ignore for a while if you prefer. Above I write lots of things like i in 1:length(x) to figure out how long a loop should be. Often, people prefer using the seq function to do this, and will write something like i in seq(x). This is slightly faster and somewhat easier to read, and so is very popular. It is, however, a Very Bad Idea. Consider the following:
empty.vector <- numeric(0)
for(i in seq(empty.vector))
print("uh-oh")Two things to note: you can skip the {} in loops as well, if you prefer (I often do), but more importantly this code loops even though there’s nothing for it to loop across! If you read the help file for seq incredibly carefully, you’ll see that this is not a bug, but rather a feature of seq. Its default behavior is to return 1:length(x) when x has more than one element, but to return 1:x if (length(x)==1). Try change the above so that empty.vector <- 10 and see what happens. This can cause Very Bad Things that will take you hours to find the solution to. Luckily, the R developers have come up with a solution: seq_along and seq_len. You could just specify arguments to seq, but why not use those two functions instead: your code will be fast, safe, and (to be frank) it’s a bit of a signal to others that you’re a careful programmer who knows what they’re doing. Writing careful, defensive code (like this) that catches potential errors from the user before they cause a problem is a Very Good Idea.
D.2.4 Functions
We’ve been using functions throughout these sessions, and it’s natural to want to know how to write your own. Here is a simple example:
double <- function(x){
doubled <- x * 2
return(doubled)
}
double(16)
#> [1] 32The function function (confusing, I know, sorry) tells R we are defining a new function, and we give it as an argument all the arguments our new function will take. After that comes a block, which is often called the body of the function, that does all the work. Once we’ve done all the work in our function, we use the return function to spit out the value of whatever variable we give it. Thus, on the final line, when we call our new function, we get the number 32 spat out.
In R, all functions are call-by-value. This means that the arguments to functions are copies of the objects that were passed to the function, not the objects themselves, and so anything done to those copies doesn’t affect the real objects themselves. This sounds complicated, so look at the following example:
x <- 4
double <- function(x){
x <- x * 2
return(x)
}
double(x)
#> [1] 8
print(x)
#> [1] 4As you can see, the value of x hasn’t changed, even though it has within our double function. This is because two variables called x very briefly existed within R: one inside the scope of the function double as it was called, and the other within your workspace. When the double function returned, it spat out the value of x within its scope and then everything within its world was destroyed (“went out of scope”, which means essentially the same thing). Thus x within your workspace was never changed—only a copy of it was modified, and it doesn’t matter in the slightest to it that another variable called x briefly existed in another scope. All R packages have their own scope; if you read on the Internet someone writing something like pez::fingerprint.regression the :: operator if being used to mean “the pez package has a function called fingerprint.regression”. If pez is attached (you’ve called library(pez)), then the :: is unnecessary9. ::: is sometimes used, and allows you access to something in a package even if the author of the package didn’t export that function (they didn’t intend you to use it; you will learn about this when you make a package later), and is best avoided if possible.
Writing functions serves a number of purposes. By breaking your work up into chunks, and giving those chunks names, it makes it easier to follow the flow of your code. It also means you write DRY code (Don’t Repeat Yourself): the moment you need to do something twice in a script, write a function that does what you need and use that instead. You’ll only have to write out that piece of code once (making your script shorter), it’ll be clearer to see what your script is doing if you give your function a sensible name, and it’ll make it easier to spot how your code fits together and speed it up or re-write it later. Once you write a function, and you know it works, you can forget about it. You can forget the details of that code (abstract away the details), and focus on the things that matter. I constantly rely on code that I wrote and unit tested (see below) years ago; I couldn’t tell you how it works now, and frankly I don’t care, and I’ll never have to.
D.2.5 Arguments and invisibility
When you call a function, you can provide values for arguments using their names, by position, or go with the authors’ defaults. This is easier to understand with examples, and below I show how to define defaults (look at the function definition) and then make use of default (a), position (b) and named (c) arguments:
change.text <- function(text, before="Will says", after="", upper=FALSE){
text <- paste(before, text, after)
if(upper)
text <- toupper(text)
return(text)
}
change.text("brush your teeth") # (a)
#> [1] "Will says brush your teeth "
change.text("brush your teeth", "Will's mum says") # (b)
#> [1] "Will's mum says brush your teeth "
change.text("ALRIGHT MUM", upper=TRUE) # (c)
#> [1] "WILL SAYS ALRIGHT MUM "It is possible to specify that arguments can only be one of several options using match.arg:
change.text <- function(text, person=c("will","mum")){
person <- match.arg(person)
text <- paste(person, "says", text)
return(text)
}
change.text("hi") # Default is first element
#> [1] "will says hi"
change.text("hi", "will") # Fine
#> [1] "will says hi"
change.text("hi", "dave") #Error!
#> Error in `match.arg()`:
#> ! 'arg' should be one of "will", "mum"It’s also possible to invisibly return. This means that a function will only return a value if it’s asked to store it into a variable10 and so nothing will be printed in your R console. This is used a lot by plotting functions like boxplot, and gives you a way to give details to the user (e.g., how many numbers are in each category) without bothering them with details they don’t want.
bond.james.bond <- function(x) invisible(x)
felix.leiter <- function(x) return(x)
bond.james.bond(10)
secret <- bond.james.bond(10)
print(secret)
#> [1] 10
felix.leiter(10)
#> [1] 10The code snippet above shows how to use invisible returns, and one more thing: it’s possible to define a function on a single line. In fact, it’s possible to use functions without even giving them names, and we’ll learn about these so-called lambda functions soon enough.
D.2.6 Bonus information
The following contains no exercises. But I absolutely promise you that, at some point, you will want to read through the information below.
Pre-allocation
Your loops will go a lot faster in R if you pre-allocate your output variables. For example:
loop.length <- 50000
t <- 1
for(i in seq_len(loop.length))
t[i] <- 10
#...quite slow...
t <- numeric(loop.length) # (!)
for(i in seq_along(loop.length))
t[i] <- 10
#...very fast!...The only difference between these two bits of code is that in the second example I pre-allocated (!) the vector t. What happens when you add things onto the end of a variable (as we do in the first example) is R has to copy that variable every time it wants to extend it. This means that it takes longer and longer (iteration 1: copy a vector of length 1 into a new vector of length 2, iteration 1000: copy a vector of length 1000 into a new vector of length 1001…). Allocating all that memory at the beginning of the loop by telling R you want a numeric vector of length loop.length saves us all this copying. Make sure you always pre-allocate loops: go back over your answers to the exercises above and see if you can figure out a way to make some of them more efficient in this way.
Also, as a fun bonus, see if you can figure out what seq_len and seq_along do. They’re pretty neat, and are useful functions to have under your belt when it comes to writing loops “in the field”. To see why I like seq_along, see what happens if you try to write a loop using 1:length(vector) when the vector is of length 0…
D.2.6.1 Advanced debugging
All programmers make mistakes, and the difference between a good programmer and a bad programmer is how quickly they can find them. Programmers call tools to help them find the slowest parts of their code profilers, and the tools to find bugs (mistakes, errors, etc.) debuggers. “Premature optimization is the root of all evil” is an old programmer’s saying; I would advise you to steer clear of profilers and the obsession with making your code fast, and instead focus on finding and fixing bugs. What follows is less of an exercise and more of a suggestion of a set of tools you should experiment with if you’re feeling confident.
R has a fantastic all-round debugging tool: browser. browser stops the execution of your code wherever it is called, and gives you control of the R prompt again so you can inspect what’s going on with your code, run new lines, etc. It also lets you execute the next line of code, continue, step into a function call (open browser inside whatever function a particular line calls). You can run each of these commands, and a few others (check the help file) by just typing their first letter (e.g., n) at the browser prompt. Be careful: if you have a variable called n, typing n and pressing enter will just run the next line; you have to print(n) (or pick a better variable name…). For example:
bad.sum <- function(x){
output <- 0
for(each in x){
if(is.character(each))
browser()
output <- output + each
}
return(output)
}
bad.sum(1:5) # all is well
bad.sum(c(1,3,5,"this.will.crash"))Run the above code and you will quickly get a feel for how this works. You can trigger browser to be called whenever an error is called by running options(error=recover). recover will be triggered whenever an error occurs, then wraps up whatever happened just before the crash and give you the option of calling browser at whatever level of the call stack you choose. What is a call stack? Each time you call a function, it gets added onto a stack, and the functions and their scopes get resolved with the deepest calls first. A stack overflow is when too many calls are made for the computer to resolve them, and is something all programmers fear because it can, in other languages, use all your computer’s memory and crash everything—hence the website of the same name is dear to many programmers. Stack overflows are, therefore, also problems for programmers who rely on recursion (which we covered in the bonus exercise of session two). Recursion, since it relies on functions, is another trick of the functional programmer’s trade. Try running bad.sum without my browser line once you’ve turned on recover; this will also make it immediately obvious what a call stack is. options(error=NULL) will turn all of this off, by the way.
All this re-writing of functions to insert browser can get tedious. Sometimes you want to put a browser statement inside code in a package you’re using; perhaps you’re getting an obscure error message and you could diagnose it in a few moments if you knew what they were doing with your data internally (I do this a lot, and it saves me emailing people for help). trace(name.of.function, edit=TRUE) will open up file window with all the code for that function which you can edit at will, inserting a browser statement or doing whatever you want. You then just call untrace(name.of.function) and everything is set back to how it was before you edited the function. You can use this trick on a function in another package—but do remember just typing the name of a function spits out all its code for you.
So feel free to read these footnotes, but don’t be concerned if they don’t make much sense.↩︎
Well, almost. Computers care a great deal about whether a number is an integer or a floating point value; math on floating point numbers is much harder for computers and they have dedicated processors just for it. My advice is to ignore it (for now), but to remember that this is why sometimes vectors are printed as
2and other times as2.0;Rcares a great deal, internally, about what is going on. The brave can read David Goldberg’s “What every computer scientist should know about floating-point arithmetic” (ACM Computing Surveys 23.1 (1991): 5-48) once the course is over; if you make it to the end please email me as I have a number of programming projects for you to work on!↩︎Technically it’s a way of calling a function—worry about this once we’ve covered functions.↩︎
NOT
=.=is sort-of-nearly the same as<-, so be careful not to type==instead of=by mistake. While we’re here, don’t use=instead of<-; in very specific circumstances the two do not do the same thing and it will cause you a headache. Use=only when giving function arguments (more on that in a later session), and<-all the rest of the time. If you want to know the difference between=and<-, ask me during the final session in this section: it’s an “interesting” story and involves the history ofRand the design of computer keyboards in the 1960s…↩︎Those of you coming from computer science will find the use of the term ‘array’ in this context confusing; sorry about that, don’t shoot the messenger.↩︎
See my other footnote about operators; behind the scenes it calls the function
is.elementfor you.↩︎It could, of course, return one list that contains multiple things in it, and this is common.↩︎
The eagle-eyed will be confused that you don’t have to write
library("pez")here (although you can). Internally,libraryis coercing an expression (pez) into acharacter("pez"). It’s a lot of hassle just to save you two key-presses.↩︎Attach is both a technical term and a function. In some introductory courses you will be encouraged to use
attach: it is a Very Bad Idea to do so. You are creating another scope, as described above, and so copying variables and making all kinds of nightmares for yourself as you (and the poor developers you rely on) can’t easily distinguish between editing your data or a copy of your data. Use thewithfunction or thedataargument instead (see last session).↩︎Sort of. It suppresses
printbeing called on things that are returned into the workspace. How it does that is complicated and requires the function to be hard-coded intoR; don’t worry about the details.↩︎