फंक्शनल प्रोग्रामिंग क्या है?
हमारे उद्देश्य के लिए ये कुछ मुख्य विशेषताएँ हैं:
लेकिन R कोई शुद्ध फंक्शनल भाषा नहीं है (जैसे Haskell के विपरीत)।
R को एक व्यावहारिक भाषा मानिए, जो धीरे-धीरे ऐसी जगहों पर फंक्शनल क्षमताएँ देती जा रही है जहाँ ये काम पूरा करने का साफ और आसान तरीका हैं। आम तौर पर लक्ष्य डेटा विश्लेषण होता है, और प्रोग्रामिंग उस तक पहुँचने का बस एक साधन है।
फंक्शनों को जोड़ने के लिए पाइप |> का इस्तेमाल कई भाषाओं में तेज़ी से आम होता जा रहा तरीका है।
संक्षेप में:
कुछ प्रोग्रामर कोड देखते समय पाइप को "फिर" की तरह पढ़ना मददगार मानते हैं: पहले चरण 1 कीजिए, फिर चरण 2 कीजिए...
नीचे दिया गया उदाहरण इस सिंटैक्स को दिखाता है।
library(stringr)
"Monday-25-February" |>
str_split("-") |>
unlist() |> # [1] "Monday" "25" "February"
str_sub(1, 3) |> # [1] "Mon" "25" "Feb"
str_to_upper() |> # [1] "MON" "25" "FEB"
str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"
इस तरह का पाइप Base R में v4.1.0 में जोड़ा गया था, और इसे कहीं-कहीं "नेटिव पाइप" कहा जाता है।
RStudio एडिटर (और कई अन्य भी) उपयुक्त स्पेस के साथ पाइप जोड़ने के लिए Ctrl-Shift-M को कीबोर्ड शॉर्टकट की तरह इस्तेमाल करते हैं।
हमने पहले देखा था कि R में बहुत से फंक्शन "वेक्टराइज़्ड" होते हैं: वे पूरे वेक्टर पर काम करते हैं, और उनके लिए अलग से लूप लिखने की ज़रूरत नहीं पड़ती।
जो फंक्शन सिर्फ वेक्टराइज़्ड फंक्शन इस्तेमाल करते हैं, वे भी अपने आप वेक्टराइज़्ड हो जाते हैं। एक छोटे उदाहरण में:
triple <- function(x) return(3 * x)
triple(1:10)
[1] 3 6 9 12 15 18 21 24 27 30
लेकिन ज़्यादा जटिल फंक्शन वेक्टर इनपुट नहीं ले पाते, जैसे अगर उनमें लूप या if-else ब्लॉक हों।
हमें फंक्शनों को वेक्टर पर लगाने का एक ज़्यादा सामान्य तरीका चाहिए (कुछ अन्य भाषाओं के map() की तरह)।
चूँकि R की मूल भाषा में कई तरह की डेटा संरचनाएँ हैं, इसलिए उन पर काम करने के लिए *apply() फंक्शनों का एक पूरा परिवार भी है।
तकनीकी रूप से इन्हें प्रोग्रामर अक्सर "हायर-ऑर्डर फंक्शन" कहते हैं और गणितज्ञ "फंक्शनल", क्योंकि ये फंक्शनों को आर्गुमेंट की तरह लेते हैं।
अभी के लिए lapply() और sapply() पर ध्यान दीजिए।
lapply()"लिस्ट-अप्लाई" लिस्ट इनपुट पर काम करने के लिए बनाया गया है, लेकिन वेक्टर ज़रूरत पड़ने पर चुपचाप लिस्ट में बदल दिए जाते हैं।
आउटपुट एक लिस्ट होता है, इसलिए वेक्टर पाने के लिए इसे खोलना पड़ सकता है:
h <- function(size) {
switch(size,
"small" = 6,
"medium" = 8,
"large" = 10
)
}
v <- c("small", "large", "medium")
h(v) # bad use of an unvectorized function
#> Error in switch(size, small = 6, medium = 8, large = 10) :
#> EXPR must be a length 1 vector
lapply(v, h) # output is a list
#> [[1]]
#> [1] 6
#> [[2]]
#> [1] 10
#> [[3]]
#> [1] 8
lapply(v, h) |> unlist() # output is a vector
#> [1] 6 10 8
जो लोग Switch कॉन्सेप्ट जानते हैं, वे नोटिस कर सकते हैं कि हम स्केलर switch की जगह dplyr लाइब्रेरी के वेक्टराइज़्ड recode_values() का भी इस्तेमाल कर सकते हैं।
वेक्टराइज़ेशन अच्छा है, और आधुनिक R इसे पाने के कई तरीके देती है।
library(dplyr)
h <- function(size) {
recode_values(size,
"small" ~ 6,
"medium" ~ 8,
"large" ~ 10
)
}
v <- c("small", "large", "medium")
h(v)
#> [1] 6 10 8
sapply()"सिंप्लिफाइड-अप्लाई" lapply जैसा ही काम करता है, लेकिन साथ ही रिटर्न वैल्यू को आसान बनाने की कोशिश करता है: जैसे लिस्ट की जगह नाम वाला वेक्टर।
स्थिति के हिसाब से unname() का इस्तेमाल करना पड़ सकता है:
sapply(v, h)
small large medium
6 10 8
sapply(v, h) |> unname()
#> [1] 6 10 8
purrr लाइब्रेरीlapply और sapply सरल हैं, ज़्यादातर R उपयोगकर्ताओं के लिए परिचित हैं, और वेक्टर तथा साधारण लिस्ट के लिए आज भी काफी लोकप्रिय हैं।
Base R में कई दूसरे apply फंक्शन भी हैं। इस तरह की उलझन के लिए सरल और ज़्यादा एक जैसा विकल्प देना Tidyverse का एक मुख्य लक्ष्य है।
अब purrr पैकेज है, जो map() जैसे कई अतिरिक्त विकल्प देता है, और साथ ही डिज़ाइन में एकरूपता तथा एरर संदेशों की स्पष्टता भी बेहतर बनाता है।
Base R की तुलना में बैकवर्ड कंपैटिबिलिटी की बाधा कम होने के कारण, यह पैकेज R को अन्य भाषाओं में मिलती आधुनिक फंक्शनल प्रोग्रामिंग के काफी करीब ले आता है।
purrr में कई और अलग-अलग तरह के फंक्शन हैं, जिन्हें आसानी से सारांश में नहीं समेटा जा सकता।
लाइब्रेरी को समझने में मदद करने वाले संसाधन:
map() फंक्शन इनपुट के हर एलिमेंट पर एक फंक्शन लगाता है, और हमेशा एक लिस्ट लौटाता है।
अब तक तो यह lapply() जैसा ही लगता है, बस नया नाम और शायद बेहतर एरर संदेशों के साथ।
लेकिन map() एक बड़े परिवार का बस एक सदस्य है।
फंक्शन चुनते समय सोचने के लिए कुछ सवाल:
map() हमेशा लिस्ट लौटाता है, लेकिन अगर आप किसी खास तरह का एटॉमिक वेक्टर चाहते हैं तो स्ट्रिंग के लिए map_chr(), पूर्णांक के लिए map_int(), वगैरह इस्तेमाल कीजिए।i लगाइए, जैसे imap()।map की जगह map2 लगाइए, जैसे दो इनपुट के लिए map2_dbl() (जैसे वैल्यू और वज़न, ताकि भारित औसत निकाला जा सके)pmap लगाइए, जैसे pmap_chr()। और हाँ, हार्डवेयर के स्तर पर समानांतरता भी संभव है।map_if(), स्थिति के इंडेक्स की शर्तें तय करने के लिए map_at(), और इनपुट पर तब तक काम करने के लिए head_while() जब तक कोई एलिमेंट प्रेडिकेट पर खरा न उतरे।map की जगह modify() फंक्शन इस्तेमाल कीजिए, या बार-बार लगाने के लिए modify_tree()।ये फंक्शन जहाँ भी संभव हो, इनपुट डेटा को पहला आर्गुमेंट बनाते हैं, ताकि वे पाइप के साथ ठीक से काम करें।
इसके अलावा, purrr (जानबूझकर) Base R से ज़्यादा सख्त है। अगर आप कुछ खास माँगते हैं, तो फंक्शन ठीक वही लौटाएँगे या एरर संदेश के साथ रुक जाएँगे।
यह सख्ती है, लेकिन मुश्किल से मिलने वाले बग कम करने में बहुत असरदार है।
map फंक्शन आम तौर पर इनपुट जितनी ही लंबाई की रिटर्न वैल्यू देते हैं।
कुछ अन्य फंक्शन डेटा की आयाम-संख्या घटाते हैं, जैसे sum()।
sum(1:100)
#> [1] 5050
ऊपर के उदाहरण में हम 100 लंबाई के वेक्टर को 1 लंबाई के पूर्णांक में बदल देते हैं। मैट्रिक्स और ऐरे पर आने वाले किसी कॉन्सेप्ट में हम देखेंगे कि आयाम घटाने की धारणा इससे ज़्यादा सामान्य है।
sum() फंक्शन पहले से मौजूद है (और बहुत से दूसरे सांख्यिकीय फंक्शन भी)।
लेकिन हमें किसी डेटा संरचना पर मनमाने आयाम घटाने वाले फंक्शन लगाने का तरीका चाहिए, map() के बराबर किसी हायर-ऑर्डर फंक्शन की मदद से।
कई अन्य भाषाओं में fold, foldl और foldr जैसे नाम इस्तेमाल होते हैं। R में इससे जुड़ा फंक्शन reduce() है, जो (एल्गोरिदम के शौकीनों में) मशहूर MapReduce फ्रेमवर्क से मेल खाता है।
पहला आर्गुमेंट, हमेशा की तरह, इनपुट डेटा है। दूसरा आर्गुमेंट दो आर्गुमेंट वाला फंक्शन है: अक्सर एक अनाम फंक्शन, लेकिन कभी-कभी + जैसा साधारण अंकगणितीय ऑपरेटर भी।
# surround an infix operator with backticks, to use as a function
reduce(1:100, `+`)
#> [1] 5050 # same as sum(1:100)
# anonymous function, alternately add and subtract
reduce(1:10, \(accum, nextval) ifelse(nextval %% 2 == 0, accum + nextval, accum - nextval))
#> [1] 7
क्या इससे कोई फर्क पड़ता है कि हम फंक्शन इनपुट की शुरुआत से लगाएँ या अंत से?
+ या * जैसे साहचर्य ऑपरेटरों के साथ नहीं।
साफ है, (1 + 2) + 3 == 1 + (2 + 3)।
इसके उलट, - और / साहचर्य नहीं हैं:
(1 - 2) - 3
#> [1] -4
1 - (2 - 3)
#> [1] 2
जिन फंक्शनों में दिशा मायने रखती है, उनके लिए वैकल्पिक .dir आर्गुमेंट है।
# default is .dir = "forward"
reduce(1:10, `-`)
#> [1] -53
reduce(1:10, `-`, .dir = "backward")
#> [1] -5
map() और map2() की तरह, दो इनपुट वेक्टर पर एक साथ काम करने के लिए reduce2() फंक्शन भी है।
R में हमें वेक्टर के साथ काम करना पसंद है। मान लीजिए, आप एक आखिरी वैल्यू की जगह बीच के सारे चरण देखना चाहते हैं।
इसके लिए accumulate() और accumulate2() फंक्शन हैं।
इनका सिंटैक्स reduce() जैसा है, बस आउटपुट टाइप बताने के लिए कुछ अतिरिक्त वैकल्पिक आर्गुमेंट होते हैं।
accumulate(1:10, `+`)
#> [1] 1 3 6 10 15 21 28 36 45 55
accumulate के कुछ आम प्रकारों के अपने फंक्शन हैं।
Vector Functions कॉन्सेप्ट में Base R के cumsum() और cumprod() की बात हुई थी, और dplyr लाइब्रेरी में संख्यात्मक औसत के लिए cummean(), तथा लॉजिकल वेक्टर के लिए cumall() और cumany() हैं।
अक्सर हम इनपुट के उन एलिमेंट को शामिल (या बाहर) करना चाहते हैं जो किसी प्रेडिकेट पर खरे उतरते हैं।
Vector Filtering कॉन्सेप्ट में हमने यह करने का एक खास तरीका देखा था।
v <- 1:3
v[v >= 2]
#> [1] 2 3
यह पुराने R में खूब इस्तेमाल होता है, लेकिन आधुनिक फंक्शनल पाइपलाइन में ठीक से नहीं बैठता।
इसीलिए purrr प्रेडिकेट फंक्शन का एक सेट देता है, जो पाइप के साथ काम करने के लिए आर्गुमेंट का आम तरीका f(input, func, ...) अपनाते हैं।
इनके नाम बाकी ज़्यादातर भाषाओं से थोड़े अलग हैं (filter और drop जैसे नामों का मतलब R में पहले से ही कुछ और है)।
इनसे जुड़े purrr फंक्शनों में keep() और discard(), तथा कई अन्य शामिल हैं।
साथ ही, एक ही लॉजिकल वैल्यू लौटाने में all() और any() की जगह लेने के लिए every(), some() और none() हैं।
हम एक चीज़ क्लब शुरू कर रहे हैं, जो ML, यानी मशीन लर्निंग, की मदद से हमारे चीज़ पसंद करने वाले ग्राहकों के पिछले रिकॉर्ड और रुचियों के आधार पर उन्हें नए चीज़ सुझाने के लिए चुनेगा।
नए सदस्यों को एक शुरुआती सर्वे भरना होता है, ताकि हम शुरू में कुछ बुनियादी डेटा इकट्ठा कर सकें। इसी से पता चला है कि कुछ ग्राहक ऐसे भी हैं जो अपनी आलोचना में बारीकी नहीं रखते और राय बहुत प्रबल तरीके से देते हैं। चूँकि इससे अधिक बारीकी वाले एल्गोरिदम में हमेशा के लिए पक्षपात पैदा हो सकता है, इसलिए इन प्रबल ग्राहकों की ज़रूरतों को संभालने के लिए एक अलग एल्गोरिदम बनाया गया है। आपको उनका डेटा संभालने के लिए कुछ हेल्पर फंक्शन बनाने के लिए कहा गया है।
हालाँकि आगे दिए गए कार्यों को हल करने के अलग-अलग तरीके हो सकते हैं, लेकिन हर कार्य को दो से अधिक हायर ऑर्डर फंक्शन के बिना हल किया जा सकता है।
रेटिंग प्रणाली पाँच सितारों पर आधारित है, जिसमें सिर्फ पूर्णांक 1:5 होते हैं।
प्रबल ग्राहक सिर्फ 1 या 5 की रेटिंग देते हैं, और हम जानना चाहते हैं कि कोई ग्राहक ऐसा व्यवहार दिखाता है या नहीं।
all_15() बनाइए, जो रेटिंग का एक वेक्टर लेता है और तब true लौटाता है जब सभी रेटिंग 1 या 5 हों, वरना false लौटाता है।
all_15(c(2, 3, 4, 4, 1))
#> [1] FALSE
all_15(c(1, 5, 5, 1, 5))
#> [1] TRUE
हमें ग्राहक के नाम (या ID) को उसकी रेटिंग से जोड़ना है।
name_customers() बनाइए, जो नामों का एक वेक्टर और रेटिंग की एक ऐरे लेता है, और उन्हें "ज़िप" करके ऐरे का ऐरे बना देता है।
अंदर वाले ऐरे के एलिमेंट के नाम name और rating होने चाहिए।
names <- c("c1", "c2")
ratings <- list(c(2, 3, 5), c(1, 1, 5))
name_customers(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c1"
#> [[1]]$rating
#> [1] 2 3 5
#> [[2]]
#> [[2]]$name
#> [1] "c2"
#> [[2]]$rating
#> [1] 1 1 5
हमें अधिक प्रबल ग्राहकों को बाकी ग्राहकों से अलग करना है।
emphatics() बनाइए, जो ग्राहक और रेटिंग लेता है।
एक ऐरे लौटाता है जिसमें सिर्फ वे ग्राहक होते हैं जो केवल 1 या 5 सितारा रेटिंग देते हैं।`
# names and ratings as in the task 2 example
emphatics(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c2"
#> [[1]]$rating
#> [1] 1 1 5
चूँकि प्रबल ग्राहक सिर्फ 1 और 5 रेटिंग देते हैं, इसलिए इन्हें 0 और 1 में बदल देना गणना के लिहाज से ज़्यादा सुविधाजनक रहेगा।
to_binary() बनाइए, जो प्रबल रेटिंग का वेक्टर लेता है।
बाइनरी रेटिंग लौटाता है, जहाँ 1 को 0 में और 5 को 1 में बदल दिया गया है।
to_binary(c(1, 1, 5, 5, 1))
#> [1] 0 0 1 1 0
जैसे-जैसे आप सभी ग्राहकों (सिर्फ प्रबल ग्राहकों की नहीं) की रुचि बेहतर समझते जाते हैं, आप समय के साथ उनके लिए सुझाव बेहतर करते जाना चाहते हैं।
satisfactions() बनाइए, जो रेटिंग का एक वेक्टर लेता है।
अब तक की संचयी औसत रेटिंग का एक वेक्टर लौटाता है।
परिणामों को 2 दशमलव स्थानों तक पूर्णांकित कीजिए।
उम्मीद है कि खुश ग्राहकों के लिए ये संख्याएँ बढ़ती रहेंगी!
satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
Exercism पर साइन अप कीजिए और R को 21 कॉन्सेप्ट111 अभ्यास तथा असली इंसानों से मिलने वाली मेंटरिंग के साथ सीखिए और उसमें महारत हासिल कीजिए, वह भी बिल्कुल मुफ्त।