توابع پیشتر در مفهوم مبانی معرفی شدند، همراه با مثالهایی مانند این:
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
اگر دقیقتر به تعریف squareit نگاه کنیم، میتوانیم بخشهای مختلفی را تشخیص دهیم:
x میگیرد.{ } نوشته میشود.squareit نسبت داده میشود.تابع در R یک شیء درجهیک است، تقریباً مثل اعداد و رشتهها. بنابراین 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 نسخهی ۴.۱.۰ به بعد) یک نحوهی نگارش کوتاهشده برای تعریف آنها وجود دارد: کلمهی 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:
حلقهها بهخودیخود کند نیستند، اما به شکلی خطرناک این امکان را فراهم میکنند که عملیات کند را داخل حلقه بگنجانید.
برداریسازی یا توابع مرتبهبالا میتوانند به شما کمک کنند تا از این نوع عامل کاراییکش در امان بمانید.