Функції ми вже розглядали в концепції Основи, і приклади були такі:
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.
Звертатися до компонентів функції в звичайній роботі доводиться рідко, але це досить легко.
class(squareit)
#> [1] "function"
formals(squareit)
#> $x
body(squareit)
#> {
#> x * x
#> }
Аргументи, отримані за допомогою formals(), схожі на масив із синтаксисом $x.
Насправді їхній тип - pairlist, особливий тип масиву, що містить пари ключ-значення.
А body - це виконуваний код із типом language (про це, ймовірно, не доведеться дбати).
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) Гедлі Вікгема:
Цикли не повільні самі по собі, але з ними небезпечно легко додати повільні операції всередину циклу.
Векторизація або функції вищого порядку допомагають захиститися від таких убивць продуктивності.