forked from gastonstat/packyourcode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtoss.Rmd
More file actions
206 lines (146 loc) · 6.46 KB
/
Copy pathtoss.Rmd
File metadata and controls
206 lines (146 loc) · 6.46 KB
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# Object toss {#toss}
## Introduction
Let's keep improving our function `toss()`, but now changing its output in order to return an object of class `"toss"`.
## Motivation for object `"toss"`
We finished the previous chapter with the following `toss()` function, which actually corresponds to the `toss` _method_ for objects of class `"coin"`:
```{r}
toss.coin <- function(x, times = 1) {
sample(x$sides, size = times, replace = TRUE, prob = x$prob)
}
```
Let's use `toss()` to flip a coin 10 times:
```{r}
set.seed(321)
acoin <- coin(c('heads', 'tails'))
toss10 <- toss(acoin, times = 10)
toss10
```
Having obtained several tosses, we can calculate things like 1) the total number of tosses, 2) the total number of `heads`, and 3) the total number of `tails`:
```{r}
# number of tosses
length(toss10)
# total number of heads
sum(toss10 == 'heads')
# total number of tails
sum(toss10 == 'tails')
```
In general, when tossing a coin, we are not only interested in keeping track of such tosses; we would also like to know (or keep track of) the number of tosses, the number of heads, and the number of tails. Consequently, it would be nice to have another class of object for this purpose.
How do you know that you need this new object class?
Well, this is precisely an example that illustrates the process of programming in general, and OOP in particular. This kind of decisions require some (or sometimes "a lot" of) thinking, and brainstorming time. To be honest, while I was writing this book and playing with `"coin"` objects and their tosses, I decided that it would be convenient to have an object of class `"toss"` containing the following information:
- all the outcomes from the series of tosses
- the total number of tosses
- the total number of heads
- the total number of tails
The most flexible type of data structure in R to store other data structures is a `list`. Having a vector of tosses, we can use a list to keep all the desired information:
```{r}
flips <- toss(coin1, times = 6)
a <- list(
tosses = flips,
total = length(flips),
heads = sum(flips == coin1$sides[1]),
tails = sum(flips == coin1$sides[2])
)
a
```
The idea is to be able to invoke `toss()`, and then obtain an object like the list `a` in the above code. But do it in such a way that the output is an object of class `"toss"`.
### Auxiliary Constructor
For convenience purposes, we can write an __auxiliary constructor__ function, which I will call `make_toss()`. This function will take an input vector (i.e. a character vector with `"heads"` and `"tails"` elements), and it will return an object of class `"toss"`:
```{r}
# auxiliary constructor function
make_toss <- function(coin, flips) {
res <- list(
coin = coin,
tosses = flips,
total = length(flips),
heads = sum(flips == coin$sides[1]),
tails = sum(flips == coin$sides[2]))
class(res) <- "toss"
res
}
```
## Main Function `toss()`
Now that we have the auxiliary function `make_toss()`, we can encapsulate it in a _master_ function `toss.coin()`:
```{r toss-ver1}
toss.coin <- function(x, times = 1) {
flips <- sample(x$sides, size = times, replace = TRUE, prob = x$prob)
make_toss(x, flips)
}
```
This is how `toss()` works:
```{r}
set.seed(2233)
fair <- coin()
toss(fair, times = 5)
```
You may ask: "Why do we need a function `make_toss()`, and another function `toss()`?". Can't we just write a single function `suppertoss()` that does everything at once?:
```{r}
# can't we just put everything in one function?
supertoss <- function(x, times = 1) {
flips <- toss(x, times = times)
res <- list(
coin = x,
tosses = flips,
total = length(flips),
heads = sum(flips == x$sides[1]),
tails = sum(flips == x$sides[2]))
class(res) <- "toss"
res
}
```
The short answer is: yes, you can. And probably this is what most beginners tend to do. The reason why I decided to break things down into simpler and smaller functions is because I went already through a couple of implementations, and realized that it was better to have the auxiliary function `make_toss()`. Also, it is good practice to write short functions that preferably do one thing.
Here's a brief recap of the main functions we have so far:
- `coin()` is a constructor function to create objects of class `"coin"`.
- `toss()` is a generic `"toss"` method.
- `make_toss()` is an auxiliary function that takes a `"coin"` and a vector of
flips, and which produces an object `"toss"`.
- `toss.coin()` is the specific `"toss"` method to be used on `"coin"` objects.
## Upgrading `toss()`
Let's consider our `quarter` coin, and apply `toss()` on it:
```{r}
quarter1 <- coin(c("washington", "fort"))
class(quarter1) <- c("quarter", "coin")
quarter1
toss(quarter1, times = 4)
```
\bigskip
`toss()` is working as expected, and you can try it with different values for `times`. The only issue is that a distracted user could pass an unexpected value for the argument `times`:
```{r}
toss(quarter1, times = -4)
```
R produces an error when `times = -4`, but it's an error that may not be very helpful for the user. The error message clearly says that `'size'` is an invalid argument, but `toss()` just has one argument: `times`.
To be more user friendly, among other reasons, it would be better to check whether `times` has a valid value. One way to do that is to include a conditional statement like the following one:
```{r}
toss.coin <- function(x, times = 1) {
if (times <= 0) {
stop("\nargument 'times' must be a positive integer")
}
flips <- sample(x$sides, size = times, replace = TRUE, prob = x$prob)
make_toss(x, flips)
}
# this works ok
toss(quarter1, 5)
# this doesn't work, but the error message is clear
toss(quarter1, -4)
```
Once again, it is good practice to write short functions that preferably do one thing. In this case, we could define a checking function `check_times()` to make sure that `times` has a valid value:
```{r}
# auxiliary function to check 'times' input
check_times <- function(times) {
if (times <= 0 | !is.numeric(times)) {
stop("\nargument 'times' must be a positive integer")
} else {
TRUE
}
}
```
Once `check_times()` has been defined, we can include it inside `toss()`:
```{r}
toss.coin <- function(x, times = 1) {
check_times(times)
flips <- sample(x$sides, size = times, replace = TRUE, prob = x$prob)
make_toss(x, flips)
}
toss(quarter1, 5)
```
### In Summary
The more you understand a problem (i.e. phenomenon, process), the better you will be prepared to design objects, and program their corresponding methods.