ट्रैक
/
R
R
/
सिलेबस
/
फंक्शनल प्रोग्रामिंग
फं

फंक्शनल प्रोग्रामिंग में R

1 अभ्यास

फंक्शनल प्रोग्रामिंग के बारे में

फंक्शनल प्रोग्रामिंग क्या है?

यह एक बड़ा सवाल है, जिसका जवाब लंबा होगा (विकिपीडिया पर एक जवाब है, और Advanced R पर दूसरा)।

हमारे उद्देश्य के लिए ये कुछ मुख्य विशेषताएँ हैं:

  • प्योर फंक्शन, जो इनपुट को हमेशा एक ही तरह से आउटपुट में बदलते हैं और कोई साइड इफेक्ट नहीं करते।
  • अपरिवर्तनीय डेटा।
  • फर्स्ट-क्लास फंक्शन, जिन्हें किसी भी और टाइप की तरह वैल्यू मानकर इस्तेमाल किया जा सकता है।
  • हायर-ऑर्डर फंक्शन, जो दूसरे फंक्शन को आर्गुमेंट या रिटर्न वैल्यू के रूप में लेते हैं।

लेकिन R कोई शुद्ध फंक्शनल भाषा नहीं है (उदाहरण के लिए Haskell के उलट)।

R को एक व्यावहारिक भाषा मानिए, जो धीरे-धीरे ऐसे मौकों पर फंक्शनल क्षमताएँ देती जा रही है जहाँ ये किसी काम को साफ और आसान तरीके से निपटाने का ज़रिया बनती हैं। आम तौर पर उद्देश्य डेटा विश्लेषण होता है, और प्रोग्रामिंग उस तक पहुँचने का एक ज़रिया भर है।

पाइप

फंक्शन जोड़ने के लिए पाइप |> का इस्तेमाल कई भाषाओं में तेज़ी से आम तकनीक बनता जा रहा है।

संक्षेप में:

  • पाइप के बाईं ओर का परिणाम दाईं ओर वाले फंक्शन का पहला आर्गुमेंट बन जाता है (यहाँ "दाईं ओर" से मतलब उन लाइन ब्रेक को छोड़कर है जो रखना ज़रूरी नहीं)।
  • बाकी बचे आर्गुमेंट फंक्शन कॉल में शामिल किए जा सकते हैं।
  • R में पाइप हमेशा फंक्शन के कॉल पर लगाना होता है, सिर्फ नाम पर नहीं, इसलिए ब्रैकेट हमेशा लगाने पड़ते हैं (कुछ और भाषाओं में ऐसा नहीं होता)।

कुछ प्रोग्रामर कोड देखते समय पाइप को "then" बोलना मददगार मानते हैं: पहले स्टेप 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 में जोड़ा गया, और इसे "नेटिव पाइप" भी कहा जाता है।

पहले पाइपिंग magrittr पैकेज के %>% से की जाती थी (यह नाम कला-प्रेमियों के लिए एक श्लेष है)। "magrittr पाइप" अब भी उपलब्ध है, लेकिन नेटिव पाइप से बाहरी पैकेज की ज़रूरत नहीं पड़ती, और उसका कार्यान्वयन कुछ बारीकियों में आसान है।

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()

"List-apply" को लिस्ट इनपुट पर काम करने के लिए बनाया गया है, लेकिन वेक्टर को ज़रूरत पड़ने पर चुपचाप लिस्ट में बदल दिया जाता है।

आउटपुट एक लिस्ट होता है, इसलिए वेक्टर पाने के लिए इसे खोलना पड़ सकता है:

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()

"Simplified-apply" 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 में कई और अप्लाई फंक्शन भी हैं:

  • vapply() sapply() जैसा ही है, लेकिन इसमें आउटपुट का फॉर्मैट बताने के लिए एक ज़रूरी पैरामीटर भी होता है।
  • apply() मैट्रिक्स और ज़्यादा आयाम वाले ऐरे के लिए।
  • mapply() एक साथ कई लिस्ट या वेक्टर आर्गुमेंट के लिए।
  • tapply() समझाना थोड़ा मुश्किल है, लेकिन यह ग्रुपिंग ऑपरेशन और असमान लंबाई वाले ऐरे के साथ काम करता है।
  • vectorize() काफी रहस्यमय फंक्शन है, जो लगता है mapply() के ऊपर एक रैपर है।

अब बहुत हुआ!

इस तरह की उलझन के लिए ज़्यादा सरल और एक जैसा विकल्प देना Tidyverse का एक मुख्य लक्ष्य है।

अब purrr पैकेज है, जो map() जैसे कई अतिरिक्त विकल्प देता है, और साथ ही डिज़ाइन में एकरूपता तथा एरर संदेशों की स्पष्टता भी सुधारता है।

बैकवर्ड कंपैटिबिलिटी की बाध्यता base R से कम होने के कारण, यह पैकेज R को दूसरी भाषाओं में मिलने वाली आधुनिक फंक्शनल प्रोग्रामिंग के काफी करीब ले आता है।

purrr में कई और तरह-तरह के फंक्शन हैं, जिनका संक्षेप में बयान करना आसान नहीं है। लाइब्रेरी को समझने में मदद करने वाले संसाधनों में शामिल हैं:

Note

purrr लाइब्रेरी लगभग पूरी तरह एक-आयामी डेटा पर काम करती है: वेक्टर और लिस्ट।

हम आगे के एक कॉन्सेप्ट में देखेंगे कि dplyr लाइब्रेरी 2-D डेटाफ्रेम के लिए भी वही काम करने की सुविधा देती है।

कई तरह के मैप फंक्शन

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 से ज़्यादा सख्त है। अगर आप कुछ खास माँगते हैं, तो फंक्शन ठीक वही लौटाएँगे या एरर संदेश के साथ रुक जाएँगे। यह सख्ती है, लेकिन मुश्किल से पकड़ में आने वाले बग कम करने में बहुत कारगर।

रिड्यूस फंक्शन

मैप फंक्शन आम तौर पर इनपुट जितनी ही लंबाई की रिटर्न वैल्यू देते हैं।

कुछ और फंक्शन डेटा के आयाम कम कर देते हैं, जैसे 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 के कुछ आम रूपों के लिए अलग फंक्शन हैं। वेक्टर फंक्शन कॉन्सेप्ट में Base R के cumsum() और cumprod() की चर्चा थी, और dplyr लाइब्रेरी में संख्यात्मक औसत के लिए cummean(), तथा लॉजिकल वेक्टर के लिए cumall() और cumany() हैं।

फ़िल्टर फंक्शन

आम तौर पर हम इनपुट के उन एलिमेंट को शामिल (या बाहर) करना चाहते हैं जो किसी प्रेडिकेट पर खरे उतरते हैं।

वेक्टर फ़िल्टरिंग कॉन्सेप्ट में हमने यह करने का एक आम तरीका देखा था।

v <- 1:3
v[v >= 2]
#> [1] 2 3

पारंपरिक R में यह बहुत इस्तेमाल होता है, लेकिन आधुनिक फंक्शनल पाइपलाइन में यह ठीक से बैठता नहीं।

इसलिए purrr प्रेडिकेट फंक्शन का एक समूह देता है, जो पाइप के साथ काम करने के लिए f(input, func, ...) वाली आम आर्गुमेंट परंपरा अपनाते हैं।

नाम ज़्यादातर दूसरी भाषाओं से थोड़े अलग हैं (R में filter और drop जैसे नाम पहले से ही बिल्कुल अलग मतलब रखते हैं)।

इससे जुड़े purrr फंक्शन में keep() और discard() शामिल हैं, और भी कई हैं।

इसके अलावा, एक ही लॉजिकल वैल्यू लौटाने में all() और any() की जगह लेने के लिए हमारे पास every(), some() और none() हैं।

रिकर्शन

ज़्यादातर फंक्शनल भाषाएँ लूप के विकल्प के रूप में रिकर्शन को प्राथमिकता देती हैं।

R के पास और भी, और अक्सर बेहतर, विकल्प हैं: वेक्टराइज़्ड फंक्शन और हायर-ऑर्डर फंक्शन।

लेकिन R में रिकर्शन का समर्थन है, और ट्री जैसी संरचनाओं में घूमने जैसे कामों में यह उपयोगी हो सकता है।

रिकर्शन की अधिकतम गहराई डिफ़ॉल्ट रूप से 5000 होती है, जो काफी उदार है (Python में यह डिफ़ॉल्ट 1000 है)। इस डिफ़ॉल्ट को बदला जा सकता है।

टेल-कॉल ऑप्टिमाइज़ेशन R में मानक नहीं है, हालाँकि इसके लिए कुछ वैकल्पिक तरीके निकाले गए हैं।

परंपरा कहती है कि यहाँ हम फैक्टोरियल का उदाहरण दिखाएँ, जो रिकर्शन से परिभाषा का सबसे क्लासिक उदाहरण है।

fact <- function(n) ifelse(n == 0, 1, n * fact(n - 1))
fact(5)
#> [1] 120

# some more idiomatic R:
factorial(5)
#> [1] 120
prod(1:5)
#> [1] 120

आप R में रिकर्शन इस्तेमाल कर सकते हैं, और कभी-कभी यह फायदेमंद भी होता है, लेकिन अक्सर इससे आसान तरीके भी होते हैं।

GitHub के ज़रिए संपादित करें यह लिंक नई विंडो या टैब में खुलता है।

फंक्शनल प्रोग्रामिंग सीखिए