Skip to content

Commit 8d13ac8

Browse files
authored
coordinate-transformation: possible functions exercise? (#518)
* possible functions exercise * fixed config.json * fixed examples in instructions * reviewer suggestions * intro cleanup (partial) * possible functions exercise * fixed config.json * fixed examples in instructions * reviewer suggestions * intro cleanup (partial) * introductions update * various additions and cleanup * more review responses * sync docs with the exercise intro
1 parent 88cea0e commit 8d13ac8

11 files changed

Lines changed: 487 additions & 3 deletions

File tree

concepts/functions/about.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,10 @@ f(4)
175175
When we define a function, we usually bind the resulting function object to a variable:
176176

177177
```R
178-
squareit_short <- function(x) x ^ 2
178+
x <- 2
179+
180+
# function definition captures x from the environment
181+
doubleit_short <- function(y) x * y
179182
```
180183

181184
This makes it easy to use the function later in the script, but such binding is not necessary.

concepts/functions/introduction.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ f(4)
151151
When we define a function, we usually bind the resulting function object to a variable:
152152

153153
```R
154-
squareit_short <- function(x) x ^ 2
154+
x <- 2
155+
156+
# function definition captures x from the environment
157+
doubleit_short <- function(y) x * y
155158
```
156159

157160
This makes it easy to use the function later in the script, but such binding is not necessary.
@@ -205,6 +208,5 @@ Vectorization or higher-order functions can help to protect you from this type o
205208
~~~~
206209

207210
[concept-basics]: https://exercism.org/tracks/r/concepts/basics
208-
[concept-funcprog]: https://exercism.org/tracks/r/concepts/functional-programming
209211
[wiki-closure]: https://en.wikipedia.org/wiki/Closure_(computer_programming)
210212
[ref-stringr]: https://stringr.tidyverse.org/index.html

config.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,18 @@
189189
],
190190
"status": "wip"
191191
},
192+
{
193+
"slug": "coordinate-transformation",
194+
"name": "Coordinate Transformation",
195+
"uuid": "ac7ace62-9ea0-4495-a77e-2268f1652cff",
196+
"concepts": [
197+
"functions"
198+
],
199+
"prerequisites": [
200+
"vector-functions"
201+
],
202+
"status": "wip"
203+
},
192204
{
193205
"slug": "factory-sensors",
194206
"name": "Factory Sensors",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Hints
2+
3+
## 1. Scale the coordinates
4+
5+
- The `point` is a vector, scaling is by a vector `s` (either explicitly a vector, or by recycling). - You already know about element-wise multiplication of vectors.
6+
7+
## 2. Translate the coordinates
8+
9+
- Convert the dot args to a vector.
10+
- The usual [`c()`][ref-c] function is flexible enough to handle this.
11+
- Unpacking vector elements into individual variables is typically _not_ a good way to use R: think in terms of vector operations.
12+
13+
## 3. Transform a 2D point
14+
15+
- Remember: translate, _then_ scale.
16+
- The `s` argument to `transform2d()` is optional, so remember to define a default.
17+
- `transform2d()` needs to return a function which takes a `point` as its sole argument.
18+
- If still confused, read the Introduction on environments and functions as return values, and look at the examples in the instructions.
19+
- This is the sort of topic that can confusing at first, but becomes very natural with a bit of practice.
20+
21+
## 4. Transform a 3D point
22+
23+
- This is just an extension of task 3.
24+
- It relies on tasks 1 and 2 being implemented in a simple, flexible way.
25+
26+
[ref-c]: https://www.rdocumentation.org/packages/base/versions/3.3.0/topics/c
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Instructions
2+
3+
Your company has been working with mapping data to build web pages.
4+
After some discussion, a decision is made to start using R to perform some calculations dynamically.
5+
6+
Please aim to work directly with the vector points: there is no need to unpack them into separate `x` and `y` coordinates.
7+
8+
## 1. Scale the coordinates
9+
10+
Implement the `scale(point, s)` function that takes a point with arbitrary number of dimensions, and scales it by the pre-defined `s`.
11+
12+
```R
13+
# A 2D example
14+
s <- c(0.5, 0.8)
15+
point <- c(10, 5)
16+
scale(point, s)
17+
#> [1] 5 4
18+
```
19+
20+
## 2. Translate the coordinates
21+
22+
Implement the `translate(point, ...)` function that returns a new point, with each coordinate moved by the values in the ellipses.
23+
24+
This function must be able to handle points of any dimension, with the number of dot args matching that dimension.
25+
26+
```R
27+
point2d <- c(2, 3)
28+
29+
# supply dx, dy translations
30+
translate(point2d, 0.5, 0.6)
31+
#> [1] 2.5 3.6
32+
33+
point3d <- c(2, 3, 4)
34+
35+
# supply dx, dy, dz translations
36+
translate(point3d, 0.5, 0.6, 0.7)
37+
#> [1] 2.5 3.6 4.7
38+
```
39+
40+
## 3. Transform a 2D point
41+
42+
Some of your teammates are less experienced with R, so you decide to use a function closure to create reusable transformations for `{x, y}` coordinate pairs.
43+
44+
Implement the `transform2d(dx, dy, s)` function that returns a function making use of a closure to perform a repeatable 2d translation and scaling of a point.
45+
46+
The scaling argument `s` should be optional, with a default value of `1`.
47+
48+
The returned function should take a 2D point, then:
49+
50+
1. translate it by the pre-defined `dx` and `dy`
51+
2. scale it by `s`
52+
53+
```R
54+
# scale all coordinates by the same amount (2) in this example
55+
# your code needs to be able to handle a vector `s`
56+
57+
tf2d <- transform2d(0.5, 0.6, 2)
58+
class(tf2d)
59+
#> [1] "function"
60+
61+
tf2d(c(2, 3))
62+
#> [1] 5.0 7.2
63+
64+
# Accept default for `s`
65+
tf2d <- transform2d(0.5, 0.6)
66+
67+
tf2d(c(2, 3))
68+
#> 2.5 3.6
69+
```
70+
71+
The order of operations is important: translate-then-scale gives the correct result, scale-then-translate does not.
72+
In technical language, the operations _do not commute_.
73+
74+
## 4. Transform a 3D point
75+
76+
Mapping data can also contain heights, used to add shading and contours on screen.
77+
78+
Implement the `transform3d(dx, dy, dz, s)` function that returns a function.
79+
80+
The scaling argument `s` should be optional, with a default value of `1`.
81+
82+
The returned function should take a 3D point, translate it by the pre-defined values, then scale it by `s`.
83+
84+
```R
85+
# using default `s`
86+
tf3d <- transform3d(0.5, 0.6, 0.7)
87+
point <- c(2, 3, 4)
88+
tf3d(point)
89+
#> [1] 2.5 3.6 4.7
90+
91+
# using vector `s`
92+
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
93+
tf3d(point)
94+
#> [1] 1.25 2.16 3.29
95+
```
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# Introduction
2+
3+
Functions were introduced back in the [Basics][concept-basics] Concept, with examples such as this:
4+
5+
```R
6+
squareit <- function(x) {
7+
x * x
8+
}
9+
10+
squareit(3)
11+
#> [1] 9
12+
13+
# shorter form
14+
squareit_short <- function(x) x ^ 2
15+
```
16+
17+
Looking more closely at the definition of `squareit`, we can identify various parts:
18+
19+
- The function takes a formal argument `x`.
20+
- There is a function body, usually in braces`{ }`.
21+
- Once the function object is created, it is assigned to a variable `squareit`.
22+
23+
A function is a first class object in R, much like numbers and strings.
24+
Thus, `squareit <- function...` is an assignment which is syntactically just like `x <- 42`.
25+
26+
## Arguments
27+
28+
R makes no clear distinction between positional arguments and keyword arguments, in contrast to other scripting languages such as Python and Julia.
29+
30+
Function calls can pass values either positionally or by name.
31+
The latter is useful for complex functions with many arguments, where it is hard to remember their order.
32+
33+
```R
34+
f <- function(x, y) x / y
35+
36+
# call positionally
37+
f(4, 2)
38+
#> [1] 2
39+
40+
# call by name
41+
f(y = 2, x = 4)
42+
#> [1] 2
43+
```
44+
45+
### Optional arguments
46+
47+
Default argument values can be specified in the function definition, but must come after all the arguments without defaults.
48+
49+
We can then choose whether to accept the default or override it.
50+
51+
```R
52+
g <- function(x, y = 2) x / y
53+
54+
# default y value
55+
g(6)
56+
#> [1] 3
57+
58+
# explicit y value
59+
g(6, 3)
60+
#> [1] 2
61+
```
62+
63+
### Extra arguments
64+
65+
To accept an arbitrary number of additional arguments, use a `...` (ellipsis) in the definition.
66+
It is possible to convert any extra values in the function call to a vector, but please read on for an alternative way to use these "dot args" (*called "varargs" in several other languages*).
67+
68+
```R
69+
var_f <- function(x, y, ...) {
70+
print(c(...))
71+
}
72+
73+
var_f(2, 3, "opt1", "opt2")
74+
#> [1] "opt1" "opt2"
75+
```
76+
77+
## Function Environment
78+
79+
Previously, we said that the formal arguments and the body are both components of a function.
80+
81+
In fact, there is a third component: the *environment* in which the function is defined.
82+
83+
This can be illustrated with the case of nested functions:
84+
85+
```R
86+
outer_func <- function(x) {
87+
inner_func <- function(y) {
88+
x * y
89+
}
90+
91+
inner_func(3)
92+
}
93+
94+
outer_func(5)
95+
#> [1] 15
96+
```
97+
98+
The function call passes `x = 5` to the outer function, and this value is available within that function body.
99+
100+
The inner function is *part* of the outer function body, and has access to the value of `x`.
101+
Worded differently, `x = 5` is in the *environment* of the inner function.
102+
103+
Technically, this is known as a [closure][wiki-closure].
104+
105+
The environment is particularly important with dot args, as any values supplied this way can be passed through to function calls in the function body.
106+
The outer function need not know or care what the dot args mean.
107+
108+
```R
109+
f_var <- function(x, ...) {
110+
sum(x, ...)
111+
}
112+
113+
x <- c(1, 2, NA, 6)
114+
115+
# for sum(), na.rm defaults to FALSE
116+
f_var(x)
117+
#> [1] NA
118+
119+
# pass through the na.rm value
120+
f_var(x, na.rm = TRUE)
121+
#> [1] 9
122+
```
123+
124+
This technique is used extensively by Tidyverse libraries such as [`stringr`][ref-stringr].
125+
Many of the `stringr` functions are a user-friendly wrapper around low-level functions from `stringi` and base R.
126+
127+
Extra arguments supplied to the `str_*()` functions are simply passed through to those lower-level functions.
128+
129+
## Functions as return values
130+
131+
An outer function can define an inner function (either named, or anonymous as described below) and use it as the return value.
132+
133+
The returned function will include the environment in which it was defined.
134+
135+
```R
136+
times_y <- function(x) {
137+
# anonymous function - see next section
138+
\(y) x * y
139+
}
140+
141+
f <- times_y(3)
142+
class(f)
143+
#> [1] "function"
144+
145+
f(4)
146+
#> [1] 12
147+
```
148+
149+
## Anonymous Functions
150+
151+
When we define a function, we usually bind the resulting function object to a variable.
152+
153+
```R
154+
x <- 2
155+
156+
# function definition captures x from the environment
157+
doubleit_short <- function(y) x * y
158+
```
159+
160+
This makes it easy to use the function later in the script, but such binding is not necessary.
161+
A short, use-once function can be useful in the immediate context.
162+
Without name-binding, it it called an *anonymous function*.
163+
164+
Use of anonymous functions is so common that (*since R v4.1.0*) there is a shorthand syntax to define them: replace the word `function` with a backslash `\`.
165+
166+
An example of this was used in the previous section, on return values.
167+
168+
## Copy on Modify
169+
170+
R allows assignment to individual elements of a vector.
171+
If we pass in a vector as a function argument, and modify it in the function body before returning it, we get a modified vector.
172+
173+
But what happened to the original vector?
174+
175+
```R
176+
f <- function(vec) {
177+
vec[1] <- 42
178+
vec
179+
}
180+
181+
vals <- c(1, 3, 4)
182+
183+
# f() returns a modified vector
184+
f(vals)
185+
#> [1] 42 3 4
186+
187+
# the original vector is unchanged.
188+
vals
189+
#> [1] 1 3 4
190+
```
191+
192+
R is a language designed for data science.
193+
Collecting that data can cost a lot in time, effort, and potentially an eye-watering amount of money: *it is important not to corrupt it!*
194+
195+
The general policy (with a few exceptions) is *copy on modify*.
196+
If an object (such as a vector) is changed in a way that could cause later problems, R returns a *modified copy* and leaves the original untouched.
197+
198+
Copying large data structures can be computationally expensive, but this is generally the lesser evil when the alternative is data corruption.
199+
200+
~~~~exercism/caution
201+
Beware of operations that lead to repeated copying of the same data.
202+
203+
Paraphrasing (Tidyverse author) Hadley Wickham:
204+
205+
> Loops are not inherently slow, but they make it dangerously easy to include slow operations within the loop.
206+
207+
Vectorization or higher-order functions can help to protect you from this type of performance-killer.
208+
~~~~
209+
210+
[concept-basics]: https://exercism.org/tracks/r/concepts/basics
211+
[wiki-closure]: https://en.wikipedia.org/wiki/Closure_(computer_programming)
212+
[ref-stringr]: https://stringr.tidyverse.org/index.html

0 commit comments

Comments
 (0)