Функції ми вже розглядали в концепції Основи, з такими, наприклад, прикладами:
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
Придивившись ближче до визначення squareit, ми можемо виділити кілька частин:
x.{ }.squareit.У R функція є обʼєктом першого класу, так само як числа й рядки тексту (англ. string).
Отже, squareit <- function... синтаксично є таким самим присвоєнням, як і x <- 42.
R не проводить чіткого розмежування між позиційними та іменованими аргументами, на відміну від інших скриптових мов, як-от Python і Julia.
Під час виклику функції значення можна передавати позиційно або за іменем. Другий спосіб стає в пригоді для складних функцій із багатьма аргументами, де важко запамʼятати їхній порядок.
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
Типові значення аргументів можна задати у визначенні функції, але вони повинні йти після всіх аргументів без типових значень.
Далі ми можемо вибрати: прийняти типове значення чи перевизначити його.
g <- function(x, y = 2) x / y
# default y value
g(6)
#> [1] 3
# explicit y value
g(6, 3)
#> [1] 2
Щоб приймати довільну кількість додаткових аргументів, використаймо ... (три крапки) у визначенні.
Будь-які додаткові значення у виклику функції можна перетворити на вектор, але про альтернативний спосіб використати ці «крапкові аргументи» (у кількох інших мовах їх називають «varargs») читаймо далі.
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
Раніше ми казали, що і формальні аргументи, і тіло є складниками функції.
Насправді є й третій складник: середовище, у якому визначено функцію.
Це можна проілюструвати на прикладі вкладених функцій:
outer_func <- function(x) {
inner_func <- function(y) {
x * y
}
inner_func(3)
}
outer_func(5)
#> [1] 15
Виклик функції передає x = 5 у зовнішню функцію, і це значення доступне в тілі цієї функції.
Внутрішня функція є частиною тіла зовнішньої функції й має доступ до значення x.
Інакше кажучи, x = 5 перебуває в середовищі внутрішньої функції.
Технічно це називають замиканням.
Середовище особливо важливе для крапкових аргументів, адже будь-які значення, передані в такий спосіб, можна передати далі у виклики функцій усередині тіла функції. Зовнішній функції не потрібно знати чи перейматися тим, що означають крапкові аргументи.
f_var <- function(x, ...) {
sum(x, ...)
}
x <- c(1, 2, NA, 6)
# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA
# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9
Цей прийом широко використовують бібліотеки Tidyverse, як-от stringr.
Багато функцій stringr - це зручна обгортка навколо низькорівневих функцій із stringi та базового R.
Додаткові аргументи, передані функціям str_*(), просто передаються далі до цих низькорівневих функцій.
Зовнішня функція може визначити внутрішню функцію (іменовану або анонімну, як описано нижче) і використати її як повернене значення.
Повернена функція включатиме середовище, у якому її було визначено.
times_y <- function(x) {
# anonymous function - see next section
\(y) x * y
}
f <- times_y(3)
class(f)
#> [1] "function"
f(4)
#> [1] 12
Коли ми визначаємо функцію, то зазвичай привʼязуємо отриманий обʼєкт функції до змінної.
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
Це дає змогу легко використати функцію далі в скрипті, але таке привʼязування не обовʼязкове. Коротка функція, яку використовують лише раз, може стати в пригоді безпосередньо на місці. Без привʼязування до імені її називають анонімною функцією.
Анонімні функції використовують так часто, що (починаючи з R v4.1.0) існує скорочений синтаксис для їх визначення: замінімо слово function на зворотну косу риску \.
Приклад цього ми наводили в попередньому розділі, про повернені значення.
R дозволяє присвоювати значення окремим елементам вектора. Якщо ми передаємо вектор як аргумент функції й змінюємо його в тілі функції перед поверненням, то отримуємо змінений вектор.
Але що сталося з початковим вектором?
f <- function(vec) {
vec[1] <- 42
vec
}
vals <- c(1, 3, 4)
# f() returns a modified vector
f(vals)
#> [1] 42 3 4
# the original vector is unchanged.
vals
#> [1] 1 3 4
R - мова, призначена для науки про дані. Збір таких даних може коштувати багато часу, зусиль, а часом і приголомшливих грошей: важливо не пошкодити їх!
Загальний принцип (з кількома винятками) - копіювання при зміні. Якщо обʼєкт (наприклад, вектор) змінюють так, що це могло б спричинити проблеми згодом, R повертає змінену копію, а оригінал залишає недоторканим.
Копіювання великих структур даних може бути обчислювально дорогим, але зазвичай це менше зло, коли альтернатива - пошкодження даних.
Остерігаймося операцій, які призводять до повторного копіювання одних і тих самих даних.
Перефразовуючи (автора Tidyverse) Гедлі Вікема:
Цикли самі собою не повільні, але з ними небезпечно легко додати повільні операції всередину циклу.
Векторизація або функції вищого порядку можуть допомогти захиститися від таких убивць продуктивності.
Наша компанія працювала з картографічними даними, щоб створювати вебсторінки. Після деяких обговорень було вирішено почати використовувати R, щоб виконувати частину обчислень динамічно.
Будь ласка, намагаймося працювати безпосередньо з векторними точками: розпаковувати їх на окремі координати x та y не потрібно.
Реалізуйте функцію scale(point, s), яка приймає точку з довільною кількістю вимірів і масштабує її на наперед визначений s.
# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4
Реалізуйте функцію translate(point, ...), яка повертає нову точку, де кожну координату зсунуто на значення, передані через ....
Ця функція повинна вміти працювати з точками будь-якої розмірності, за умови що кількість аргументів, переданих через ..., відповідає цій розмірності.
point2d <- c(2, 3)
# supply dx, dy translations
translate(point2d, 0.5, 0.6)
#> [1] 2.5 3.6
point3d <- c(2, 3, 4)
# supply dx, dy, dz translations
translate(point3d, 0.5, 0.6, 0.7)
#> [1] 2.5 3.6 4.7
Деякі з наших колег мають менший досвід роботи з R, тож ми вирішуємо скористатися замиканням, щоб створювати перетворення для пар координат {x, y}, які можна застосовувати повторно.
Реалізуйте функцію transform2d(dx, dy, s), яка повертає функцію, що використовує замикання для багаторазового переміщення та масштабування точки.
Аргумент масштабування s має бути необовʼязковим, з типовим значенням 1.
Повернена функція повинна приймати 2D-точку, а потім:
dx та dy
s
# scale all coordinates by the same amount (2) in this example
# your code needs to be able to handle a vector `s`
tf2d <- transform2d(0.5, 0.6, 2)
class(tf2d)
#> [1] "function"
tf2d(c(2, 3))
#> [1] 5.0 7.2
# Accept default for `s`
tf2d <- transform2d(0.5, 0.6)
tf2d(c(2, 3))
#> 2.5 3.6
Порядок операцій має значення: перенесення, а потім масштабування дає правильний результат, а масштабування, а потім перенесення ні. Технічною мовою кажучи, ці операції не комутують.
Дані картографування також можуть містити висоти, які використовують, щоб додавати затінення та контури на екрані.
Реалізуйте функцію transform3d(dx, dy, dz, s), яка повертає функцію.
Аргумент масштабування s має бути необовʼязковим, з типовим значенням 1.
Повернена функція повинна приймати 3D-точку, переносити її на наперед визначені значення, а потім масштабувати її на s.
# using default `s`
tf3d <- transform3d(0.5, 0.6, 0.7)
point <- c(2, 3, 4)
tf3d(point)
#> [1] 2.5 3.6 4.7
# using vector `s`
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
tf3d(point)
#> [1] 1.25 2.16 3.29
Зареєструйтеся на Exercism, щоб вивчати й опановувати R, а також 21 концепція111 вправ та справжнє наставництво від людей, і все це безкоштовно.