फंक्शन से हमारा परिचय पहले मूल बातें कॉन्सेप्ट में हुआ था, जहाँ इस तरह के उदाहरण देखे थे:
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 है (ऐसा कुछ जिसकी आपको शायद परवाह करने की ज़रूरत नहीं है)।
Python और Julia जैसी दूसरी स्क्रिप्टिंग भाषाओं के उलट, R पोज़िशनल आर्गुमेंट और कीवर्ड आर्गुमेंट में कोई साफ अंतर नहीं करता।
फंक्शन को कॉल करते समय वैल्यू या तो पोज़िशन के हिसाब से दी जा सकती है, या नाम के साथ। नाम के साथ देना उन जटिल फंक्शन के लिए काम आता है जिनमें बहुत सारे आर्गुमेंट होते हैं और जिनका क्रम याद रखना मुश्किल हो जाता है।
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 और base 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 के लेखक) Hadley Wickham के शब्दों को थोड़ा बदलकर कहें तो:
लूप अपने आप में धीमे नहीं होते, लेकिन इनकी वजह से लूप के अंदर धीमे काम डाल देना खतरनाक तरीके से आसान हो जाता है।
वेक्टराइज़ेशन या हायर ऑर्डर फंक्शन इस तरह के परफॉर्मेंस-किलर से आपको बचाने में मदद कर सकते हैं।