توابع پیشتر در مفهوم مبانی معرفی شدند، با مثالهایی مانند این:
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 است.
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) Hadley Wickham:
حلقهها ذاتاً کند نیستند، اما به شکلی خطرناک این امکان را فراهم میکنند که کارهای کند را داخل حلقه بگنجانید.
برداریسازی یا توابع مرتبهبالاتر میتوانند کمکتان کنند از این نوع عامل کشندهی کارایی در امان بمانید.
شرکت شما در حال ساخت صفحات وب با استفاده از دادههای نقشهبرداری بوده است. پس از چند گفتوگو، تصمیم گرفته میشود که کمکم از 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 است.
تابع برگرداندهشده باید یک نقطهی دوبعدی بگیرد و سپس:
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 است.
تابع برگرداندهشده باید یک نقطهی سهبعدی بگیرد، آن را با مقادیر از پیش تعریفشده انتقال دهد و سپس با 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