ফাংশনাল প্রোগ্রামিং কী?
এটি একটি বড় প্রশ্ন, যার উত্তর অনেক লম্বা (Wikipedia একটি দেয়, Advanced R আরেকটি দেয়)।
আমাদের উদ্দেশ্যে এগুলো কিছু গুরুত্বপূর্ণ বৈশিষ্ট্য:
তবে R একটি পিউর ফাংশনাল ভাষা নয় (যেমন Haskell, তার বিপরীতে)।
R-কে একটি প্র্যাগম্যাটিক ভাষা হিসেবে দেখুন, যা (ক্রমশ) ফাংশনাল সক্ষমতা দেয় যেখানে এগুলো কোনো কাজ সম্পন্ন করার পরিচ্ছন্ন ও সহজ উপায়। সাধারণভাবে, লক্ষ্য হলো ডেটা অ্যানালাইসিস, আর প্রোগ্রামিং শুধু সেই লক্ষ্যে পৌঁছানোর একটি উপায়।
কয়েকটি ভাষায় ফাংশন সংযুক্ত করতে পাইপ |> ব্যবহার করা একটি ক্রমশ কমন টেকনিক।
সংক্ষেপে:
কোড পর্যালোচনা করার সময় কিছু প্রোগ্রামার পাইপকে "then" হিসেবে উচ্চারণ করা সহায়ক মনে করেন: ধাপ ১ করুন, তারপর (then) ধাপ ২ করুন...
নিচের উদাহরণটি সিনট্যাক্স দেখায়।
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()-এর মতো আরও কয়েকটি অপশন দেয়, সেই সাথে ডিজাইনে সামঞ্জস্য এবং এরর মেসেজে স্পষ্টতা বাড়ায়। বেস R-এর তুলনায় ব্যাকওয়ার্ড কম্প্যাটিবিলিটি দ্বারা কম সীমাবদ্ধ এই প্যাকেজ R-কে অন্য ভাষায় দেখা আধুনিক ফাংশনাল প্রোগ্রামিংয়ের অনেক কাছাকাছি নিয়ে আসে।
purrr-এ অনেক ও বৈচিত্র্যময় ফাংশন আছে, যা সহজে সংক্ষেপে বলা যায় না। লাইব্রেরিটি ঘুরে দেখতে সহায়ক রিসোর্সগুলোর মধ্যে আছে:
purrr লাইব্রেরি প্রায় সম্পূর্ণভাবে ১-মাত্রিক ডেটায় কাজ করে: ভেক্টর আর লিস্ট।
আমরা পরবর্তী একটি কনসেপ্টে দেখব যে dplyr লাইব্রেরি ২-মাত্রিক ডেটাফ্রেমের জন্য সমতুল্য কার্যকারিতা দেয়।
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-এর চেয়ে বেশি খুঁটিনাটি-সর্বস্ব। আপনি যদি নির্দিষ্ট কিছু চান, ফাংশনগুলো ঠিক সেটাই রিটার্ন করবে, নয়তো একটি এরর মেসেজ দিয়ে থেমে যাবে। কঠিন, তবে খুঁজে পাওয়া কঠিন এমন bug কমানোর জন্য খুবই কার্যকর।
ম্যাপ ফাংশনগুলোর রিটার্ন ভ্যালু সাধারণত ইনপুটের সমান দৈর্ঘ্যের হয়।
কিছু অন্য ফাংশন ডেটার মাত্রা কমায়, উদাহরণস্বরূপ sum()।
sum(1:100)
#> [1] 5050
উপরের ক্ষেত্রে, আমরা একটি ১০০ দৈর্ঘ্যের ভেক্টরকে ১ দৈর্ঘ্যের ইন্টিজারে রূপান্তর করি। আমরা ম্যাট্রিক্স ও অ্যারে নিয়ে ভবিষ্যতের একটি কনসেপ্টে দেখব যে মাত্রা-হ্রাসের ধারণাটি আরও সাধারণ।
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, ...)-এর প্রচলিত আর্গুমেন্ট কনভেনশন অনুসরণ করে।
নামগুলো বেশিরভাগ অন্য ভাষা থেকে একটু আলাদা (filter আর drop-এর মতো নাম R-এ ইতিমধ্যে সম্পূর্ণ ভিন্ন কিছু বোঝায়)।
সংশ্লিষ্ট purrr ফাংশনগুলোর মধ্যে keep() আর discard(), সাথে আরও কয়েকটি আছে।
এছাড়াও, একটি একক লজিক্যাল মান রিটার্ন করার ক্ষেত্রে all() আর any()-এর বদলে আমাদের every(), some() আর none() আছে।
বেশিরভাগ ফাংশনাল ভাষা লুপের পছন্দের বিকল্প হিসেবে রিকারশন ব্যবহার করে।
R-এর কাছে অন্য, প্রায়ই আরও ভালো অপশন আছে: ভেক্টরাইজড ফাংশন আর হায়ার-অর্ডার ফাংশন।
তবে, R-এ রিকারশন সাপোর্টেড, আর ট্রি স্ট্রাকচার ট্রাভার্স করার মতো প্রসঙ্গে এটি কাজে লাগতে পারে।
সর্বোচ্চ রিকারশন গভীরতা ডিফল্টভাবে ৫০০০, যা তুলনামূলকভাবে উদার (Python-এ ডিফল্ট ১০০০)। ডিফল্টটি ওভাররাইড করা সম্ভব।
টেইল-কল অপটিমাইজেশন 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-এ রিকারশন ব্যবহার করতে পারেন, আর কখনো কখনো এটি মূল্যবান, তবে প্রায়ই আরও সহজ উপায় থাকে।