Треки
/
R
R
/
Салабус
/
Функції
Фу

Функції у R

1 вправа

Про концепцію Функції

Функції ми вже розглядали в концепції Основи, і приклади були такі:

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.

Advanced

Звертатися до компонентів функції в звичайній роботі доводиться рідко, але це досить легко.

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 повертає змінену копію, а оригінал залишає без змін.

Копіювання великих структур даних може бути обчислювально затратним, але зазвичай це менше зло, коли альтернатива - пошкодження даних.

Caution

Остерігаймося операцій, які призводять до повторного копіювання тих самих даних.

Перефразовуючи (автора Tidyverse) Гедлі Вікгема:

Цикли не повільні самі по собі, але з ними небезпечно легко додати повільні операції всередину циклу.

Векторизація або функції вищого порядку допомагають захиститися від таких убивць продуктивності.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Функції