12  Arithmetic and variables

12.1 Arithmetic

If one opens an R terminal, one can first use it as a calculator, as the following example shows.

1+1                                                     # addition
[1] 2

In the terminal, this shows, on the one hand, the result \(2\) and, on the other hand, with [1], an indication that the result is the first entry in the result vector generated by R. We will discuss vectors in detail later. Further examples of using an R terminal as a calculator are the following.

2*3                                                     # multiplication
[1] 6
sqrt(2)                                                 # square root
[1] 1.414214
exp(0)                                                  # exponential function
[1] 1
log(1)                                                  # logarithm function
[1] 0

Table 12.1 gives an initial overview of the arithmetic operators available in Base R. In addition to the familiar operations of addition, subtraction, multiplication, division, and exponentiation, Base R also offers special operators for more advanced arithmetic computations. These include, for example, matrix multiplication, integer division (5 %/% 2 = 2), and the modulo operation, which returns the remainder of an integer division (5 %% 2 = 1).

Table 12.1: Examples of arithmetic operators in Base R.
Operation Operator
Addition +
Subtraction -
Multiplication *
Division /
Power ^
Matrix multiplication %*%
Integer division %/%
Modulo operation %%

In addition to the arithmetic operators for combining two numbers, Base R naturally also offers the possibility of evaluating standard mathematical functions. Table 12.2 gives an initial overview.

Table 12.2: Examples of mathematical functions in Base R.
Function Call
Exponential function exp(x)
Logarithm function log(x)
Absolute value abs(x)
Square root sqrt(x)
Round up ceiling(x)
Round down floor(x)
Mathematical rounding round(x)

Although the operators listed in Table 12.1 and the functions listed in Table 12.2 seem to have a somewhat different character at first glance, Base R does not formally distinguish between operators and functions. In particular, operators can also be used and understood as functions of several arguments by means of so-called infix notation. The R code below shows how the arithmetic combination \(2 + 3\) can be understood as the execution of a function of the form \(+ : \mathbb{R}^2 \to \mathbb{R}\) with the name “+”.

`+`(2,3)                                                # infix notation for 2 + 3
[1] 5

Finally, like every programming language, Base R also offers the possibility of evaluating expressions with respect to their logical content. Here, logic is to be understood in the sense of the propositional logic from Chapter 1, in which statements can take one of two values, true (TRUE) or false (FALSE). The relational operators <, <=, >, and >= listed in Table 12.3 are usually applied to numerical values. The equality operators == and != can be applied to arbitrary data structures. Finally, the operators for combining logical values, & and |, correspond to the concepts of logical conjunction (“and”) and disjunction (“non-exclusive or”) known from Chapter 1.

Table 12.3: Examples of logical operators in Base R.
Logical connection Operator
Equal ==
Unequal !=
Conjunction &
Disjunction |
Less than <
Less than or equal <=

The operators and functions listed in Table 12.1, Table 12.2, and Table 12.3 represent only a small excerpt of the operators and functions provided by Base R. A complete overview is provided by the call names(methods:::.BasicFunsList), which lists the names of all functions provided by Base R. We will get to know many of these functions later.

names(methods:::.BasicFunsList)
  [1] "$"                    "$<-"                  "["                   
  [4] "[<-"                  "[["                   "[[<-"                
  [7] "crossprod"            "tcrossprod"           "xtfrm"               
 [10] "c"                    "all"                  "any"                 
 [13] "sum"                  "prod"                 "max"                 
 [16] "min"                  "range"                "cummax"              
 [19] "rep"                  "^"                    "cos"                 
 [22] "levels<-"             "cumsum"               "asin"                
 [25] "anyNA"                "<="                   "Conj"                
 [28] "exp"                  "is.matrix"            "log10"               
 [31] "as.environment"       "ceiling"              "asinh"               
 [34] "abs"                  "as.raw"               "is.infinite"         
 [37] "is.array"             "floor"                "=="                  
 [40] "sign"                 "cummin"               "|"                   
 [43] "round"                "Re"                   "!"                   
 [46] "is.numeric"           "acosh"                "!="                  
 [49] "names<-"              "&"                    "atanh"               
 [52] "sinh"                 ">="                   "sin"                 
 [55] "*"                    "atan"                 "+"                   
 [58] "length"               "length<-"             "sinpi"               
 [61] "-"                    "is.nan"               "/"                   
 [64] "sqrt"                 "%/%"                  "as.numeric"          
 [67] "seq.int"              "trunc"                "digamma"             
 [70] "acos"                 "<"                    "as.logical"          
 [73] "cosh"                 "Mod"                  "tanpi"               
 [76] "%*%"                  "dimnames<-"           "log2"                
 [79] ">"                    "tanh"                 "is.na"               
 [82] "dim"                  "signif"               "gamma"               
 [85] "is.finite"            "as.integer"           "dim<-"               
 [88] "as.double"            "lgamma"               "Arg"                 
 [91] "log1p"                "trigamma"             "%%"                  
 [94] "tan"                  "cumprod"              "Im"                  
 [97] "expm1"                "as.call"              "log"                 
[100] "as.complex"           "cospi"                "as.character"        
[103] "dimnames"             "names"                "("                   
[106] ":"                    "="                    "@"                   
[109] "{"                    "~"                    "&&"                  
[112] ".C"                   "baseenv"              "quote"               
[115] "::"                   "<-"                   "is.name"             
[118] "if"                   "||"                   "attr<-"              
[121] "untracemem"           ".cache_class"         "substitute"          
[124] "interactive"          "is.call"              "switch"              
[127] "function"             "is.single"            "is.null"             
[130] "is.language"          "is.pairlist"          ".External.graphics"  
[133] "declare"              "globalenv"            "class<-"             
[136] ".Primitive"           "is.logical"           "enc2utf8"            
[139] "UseMethod"            ".subset"              "proc.time"           
[142] "enc2native"           "repeat"               ":::"                 
[145] "<<-"                  "@<-"                  "missing"             
[148] "nargs"                "isS4"                 ".isMethodsDispatchOn"
[151] "forceAndCall"         "Exec"                 ".primTrace"          
[154] "storage.mode<-"       ".Call"                "unclass"             
[157] "gc.time"              ".subset2"             "environment<-"       
[160] "emptyenv"             "seq_len"              ".External2"          
[163] "is.symbol"            "class"                "on.exit"             
[166] "is.raw"               "for"                  "is.complex"          
[169] "list"                 "invisible"            "is.character"        
[172] "oldClass<-"           "is.environment"       "attributes"          
[175] "break"                "return"               "attr"                
[178] "tracemem"             "next"                 ".Call.graphics"      
[181] "standardGeneric"      "is.atomic"            "retracemem"          
[184] "expression"           "is.expression"        "call"                
[187] "is.object"            "pos.to.env"           "attributes<-"        
[190] ".primUntrace"         "...length"            "Tailcall"            
[193] ".External"            "oldClass"             ".Internal"           
[196] ".Fortran"             "browser"              "is.double"           
[199] ".class2"              "while"                "nzchar"              
[202] "is.list"              "lazyLoadDBfetch"      "unCfillPOSIXlt"      
[205] "...elt"               "...names"             "is.integer"          
[208] "is.function"          "is.recursive"         "seq_along"           
[211] "unlist"               "as.vector"            "lengths"             

12.2 Precedence

An important property when using operators is their precedence, as defined by the respective programming language. These are rules specified by the developers of programming languages that determine the order of operations. From mathematics, one is especially familiar with the precedence rule “multiplication and division before addition and subtraction”. This rule says that in expressions containing both products or divisions and sums or differences, the products or divisions are carried out first, and their results then enter the formation of sums or differences second. Thus, for example, \[ 2 \cdot 5 - 3 = 10 - 3 = 7 \tag{12.1}\] and not, for instance, \[ 2 \cdot 5 - 3 \neq 2 \cdot 2 = 4. \tag{12.2}\] We assume as known that precedence rules can be emphasized or overridden by parentheses. For example, the expression in Equation 12.1 is equivalent to \[ (2 \cdot 5) - 3 = 10 - 3 = 7 \tag{12.3}\] and the calculation intended in Equation 12.2 can be made correct by placing parentheses as \[ 2 \cdot (5 - 3) = 2 \cdot 2 = 4 \tag{12.4}\] These familiar calculation rules and their overriding by parentheses are implemented correspondingly in R:

2*5 - 3
[1] 7
2*(5 - 3)
[1] 4

Another precedence rule assumed to be known is that powers have higher precedence than products or divisions and sums or differences. Thus, for example, \[\begin{equation} 2^3 + 3 = (2\cdot 2 \cdot 2) + 3 = 8 + 3 = 11 \end{equation}\] and not, for instance, \[\begin{equation} 2^3 + 3 \neq 2^{3 + 3} = 2^6 = 64. \end{equation}\] Correspondingly, in R we have

2^3 + 3
[1] 11
2^(3 + 3)
[1] 64

An important special feature of the precedence rules in R is that unary signs, that is, signs that identify a number as a negative number, also fall within the domain of operator precedence. Although one might be inclined to interpret an expression of the form \(-1^2\) as \((-1)^2 = 1\), R nevertheless follows the rule that in \(-1^2\) the power is evaluated first and the sign afterward, so that \(-1^2 = -(1^2) = -1\) holds:

-1^2                           # -(1^2)   = -1
[1] -1
(-1)^2                         # (-1)^2   =  1
[1] 1

Furthermore, in R, powers are processed from right to left, whereas products, divisions, sums, and differences are processed from left to right. For example,

2^2^3                          # 2^(2^3)  = 2^8 = 256
[1] 256
(2^2)^3                        # (2^2)^3  = 4^3 = 64
[1] 64

but

2+3/4*5                        # 2+(3/4)*5 = 2+(0.75*5) = 2+3.75 = 5.75
[1] 5.75
2+3/(4*5)                      # 2+3/(4*5) = 2+3/20 = 2+0.15 = 2.15
[1] 2.15

In general, in programming one should not guess precedence rules or try to derive them logically. Nor should one trust that the developers of a programming language will probably have implemented the precedence rules according to one’s own state of knowledge. Instead, one should always check computations critically and, to be safe, emphasize the intended calculation with parentheses once too often rather than once too little.

In addition to the precedence rules considered here for the basic arithmetic operations, R has a whole series of further rules for dealing with many other operators, of which we have so far become familiar with only very few. When becoming familiar with a new operator while learning a programming language, one should therefore also make sure to clarify the precedence of this operator. The text from the R documentation shown in Figure 12.1, which can be called with the command ?Syntax, discusses the precedence rules that apply in R in full and should be consulted frequently when working with R.

Figure 12.1: Precedence rules in R.

12.3 Variables

In programming, variables are abstract containers for data whose contents can be changed during the execution of a program. Variables are denoted by names in program code and, during program execution, have an address in working memory. Variables can be of different types. This means that variables represent different kinds of data, and only those kinds. In traditional programming languages, the type of a variable is explicitly declared. At the beginning of program execution, it is specified which kind of data a particular variable can represent. The declaration of a variable A, which, for example, can represent integers and only integers, has roughly the form

VAR A : INTEGER     # a is a variable of type integer

Subsequently, the variable A can be assigned a value of type integer, for example in the form

A := 1              # variable A is assigned the numerical value 1

In a 4GL language such as R, the variable type is usually specified directly by an initialization statement. Thus, the following R code simultaneously declares the variable a as being of type double (decimal number) with the value 1,

a = 1               # a is a variable of type double; its value is 1

In the vast majority of programming languages, the assignment command is =. R is special in this respect because it allows the additional assignment commands <- and ->: <- assigns from right to left, -> from left to right. The usual assignment from right to left is also achieved by =. The officially recommended assignment command for R is <-. This is, however, highly idiosyncratic, and we mostly use =, as in every other programming language. A first example should illustrate how to work with variables.

Example

We consider the following task:

“Lina goes to the stationery store and buys four notebooks and two pens. One notebook costs 1 euro and one pen costs 3 euros. How many items does Lina buy in total, and how many euros does Lina have to pay in total?”

To solve the task, we first define the variables number_notebooks and number_pens and assign to them their respective values from the task.

number_notebooks = 4
number_pens      = 2

After assignment, the variables with their assigned values now exist in working memory, the so-called workspace. The command ls() displays the existing usable variables in working memory.

ls()
[1] "number_notebooks" "number_pens"     

The crucial point is that the variable names can now be used like numbers in computations. print() outputs variable values in an R terminal:

total_items = number_notebooks + number_pens
print(total_items)
[1] 6

To calculate the total price, we next define the variables price_notebook and price_pen according to the task specification.

price_notebook = 1
price_pen      = 2

The total price is then calculated as follows.

total_price = number_notebooks*price_notebook + number_pens*price_pen
print(total_price)
[1] 8

Variables in the workspace can also be deleted again. rm() (remove) allows individual variables to be deleted.

rm(total_price)
ls()
[1] "number_notebooks" "number_pens"      "price_notebook"   "price_pen"       
[5] "total_items"     

rm(list=ls()) deletes all variables.

rm(list = ls())
ls()
character(0)

Variable names

In principle, one is free to choose names for variables, with minor restrictions. Admissible variable names in R

  • consist of letters, numbers, periods (.), and underscores (_),
  • begin with a letter or a period, but then not followed by a number,
  • must not be expressions already reserved in R, such as for, if, NaN, …

The help page ?reserved provides assistance with the restrictions on variable names. A function for generating a syntactically admissible variable name from arbitrary suggestions is make.names(). Its help page also describes the rules for variable naming sketched here in further detail. In general, useful variables are short, to save space in the code, and meaningful, to increase human understanding of the code. Variables usually consist only of lowercase letters and underscores.

Variable representation

We now want to examine the relationship between variable names (identifiers) and variable contents (objects in working memory) in a little more detail. The following concepts and relationships are especially important when computations reach the limits of the available working memory and must be optimized.

We begin with the concept of binding. To do so, we first consider the following variable initialization:

x = 1

Intuitively, this statement creates a variable named x with the value 1. At the technical level, two things happen (cf. Figure 12.2):

  1. R creates an object (a vector with value 1) with working-memory address lobstr::obj_addr(x).
  2. R connects this object with the name x (binding), which references the object in working memory.
Figure 12.2: Binding of a name to an object in working memory.

Now consider the following statement:

y = x

Intuitively, this creates a variable named y with a value equal to the value of x. At the technical level, a new name y is created that references the same object in working memory as x (cf. Figure 12.3).

Figure 12.3: Binding of two names to one object in working memory.

One can use lobstr::obj_addr(x) and lobstr::obj_addr(y) to verify that the addresses of the object in working memory are identical:

print(lobstr::obj_addr(x))
[1] "0x169f2a64ce8"
print(lobstr::obj_addr(y))
[1] "0x169f2a64ce8"

Thus, the relevant object is not copied when assigned to y. This saves working memory.

Furthermore, R uses the so-called copy-on-modify principle. This principle states that an object in working memory is copied and receives a new address as soon as it is modified. The original object in working memory remains unchanged. Figure 12.4 and the following R code first illustrate the copy-on-modify principle:

x = 1       # object (e.g., 0x74b) is created; x references the object's memory address
y = x       # y references the same memory address as x (0x74b)
y = 3       # y is modified; a modified copy (e.g., 0xcd2) is stored
y           # y now references (0xcd2)
[1] 3
x           # x continues to reference (0x74b)
[1] 1
Figure 12.4: Copy-on-modify. Only after changing the value assigned to the identifier y is the value copied in working memory.

The fact that objects are copied in working memory when they are changed, while the original objects are in principle preserved unchanged, is also referred to as the immutability of objects in R. However, the immutability of objects in R does not go very far, as the following example shows. First, an object of a 1 is created in working memory and can be referenced with the variable name x. By indexing (cf. Chapter 13), the object is then readdressed in working memory when an intended modification takes place.

x   = 1                     # object is created; x references the object's memory address
print(lobstr::obj_addr(x))  # object's memory address
[1] "0x169eef96890"
x[1] = 2                    # object is changed
print(lobstr::obj_addr(x))  # new memory address
[1] "0x169f0070658"

The copy-on-modify principle also applies in the reverse order. That is, if an object to which another variable name points is changed, the originally referenced object does not change. The following R code may illustrate this.

x = 1       # object (e.g., 0x74b) is created; x references the object's memory address
y = x       # y references the same memory address as x (0x74b)
x = 3       # a new object (e.g., 0x2a08) is created; x now references 0x2a08
y           # y continues to reference the original object (0x74b)
[1] 1

Finally, unbinding of objects in working memory is also possible, for example as a consequence of the following R commands (cf. Figure 12.5):

x = 1       # x references object (0x74b)
x = "a"     # x references object (0x2a08); object (0x74b) is now unreferenced
Figure 12.5: Unbinding. Allocated working memory remains that is not referenced by any identifier.

The deletion of unreferenced objects in working memory is called garbage collection. R automatically deletes unreferenced objects in working memory, although usually only when the memory is needed. In general, however, this works well, and it is usually not necessary to actively use the function gc() provided by R for garbage collection.

Representation of missing values

In data-analytic programming, one often has to deal with missing values, for example when an experimental measurement for a participant had to be aborted. To represent missing values, R uses NA (not available). If one attempts to calculate with NA, the result is usually again NA:

3 * NA       # multiplication of an NA value yields NA
[1] NA
NA < 2       # relational comparison of an NA value yields NA
[1] NA
NA^0         # missing value to the power of 0 yields 1 because every value to the power of 0 is one (?)
[1] 1
NA & FALSE   # logical conjunction with FALSE yields FALSE
[1] FALSE

One tests for NA with is.na():

x = c(NA, 5, NA, 10)  # vector with NAs
x == NA               # no testing for NAs: 5 == NA is NA, not FALSE
[1] NA NA NA NA
is.na(x)              # logical test for NA
[1]  TRUE FALSE  TRUE FALSE

In addition to NA, R also has NaN as a way to represent mathematically undefined values. In contrast to NA, NaN is always of numerical type. The results when calculating with NaN are similar to those with NA.

3 * NaN      # multiplication of a NaN value yields NaN
[1] NaN
NaN < 2      # relational comparison of a NaN value yields NA
[1] NA
NaN^0        # undefined value to the power of 0 yields 1 because every value to the power of 0 is one (?)
[1] 1
NaN & FALSE  # logical conjunction with FALSE yields FALSE
[1] FALSE

One tests for NaN with is.nan().

x = c(NaN, 5, NaN, 10)  # vector with NaNs
is.nan(x)               # logical test for NaN