ফাংশনাল প্রোগ্রামিং কী?
আমাদের আলোচনার প্রেক্ষিতে, এর কয়েকটি মূল বৈশিষ্ট্য হলো:
তবে 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 সংস্করণে যোগ করা হয়েছে, এবং এটিকে "নেটিভ পাইপ" নামেও উল্লেখ করা হতে দেখতে পারেন।
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()-এর মতো)।
যেহেতু Base 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-এ আরও অনেক অ্যাপ্লাই ফাংশন আছে। এই ধরনের বিভ্রান্তির জন্য আরও সহজ ও বেশি সামঞ্জস্যপূর্ণ একটি বিকল্প দেওয়া 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-এর চেয়ে বেশি খুঁতখুঁতে। আপনি যদি নির্দিষ্ট কিছু চান, ফাংশনগুলো ঠিক সেটাই রিটার্ন করবে, নাহলে একটি এরর মেসেজ দিয়ে থেমে যাবে। কঠিন, কিন্তু খুঁজে পাওয়া কষ্টকর বাগ কমাতে খুবই কার্যকর।
ম্যাপ ফাংশনগুলোর রিটার্ন ভ্যালু সাধারণত ইনপুটের সমান দৈর্ঘ্যের হয়।
কিছু অন্য ফাংশন ডেটার ডাইমেনশনালিটি কমিয়ে আনে, যেমন 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
কিছু কমন অ্যাকিউমুলেট টাইপের নিজস্ব ফাংশন আছে। 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, ...)-এর প্রচলিত আর্গুমেন্ট রীতিটি অনুসরণ করে।
নামগুলো বেশিরভাগ অন্য ভাষার থেকে একটু আলাদা (R-এ filter আর drop-এর মতো নামের অর্থ আগে থেকেই সম্পূর্ণ ভিন্ন কিছু)।
সংশ্লিষ্ট purrr ফাংশনগুলোর মধ্যে আছে keep() ও discard(), আরও কয়েকটি সহ।
এছাড়া একটি একক লজিক্যাল মান রিটার্ন করার ক্ষেত্রে all() ও any()-এর বদলে আমাদের আছে every(), some() ও none()।
আমরা একটি চিজ ক্লাব চালু করছি, যেখানে ML ব্যবহার করে আমাদের চিজপ্রেমী গ্রাহকদের ইতিহাস ও রুচির ভিত্তিতে তাঁদের জন্য নতুন চিজ বাছাই করা হবে।
নতুন সদস্যদের একটি প্রাথমিক জরিপ পূরণ করতে হয়, যাতে শুরুতে আমরা কিছু মৌলিক ডেটা সংগ্রহ করতে পারি। এর মাধ্যমেই দেখা গেছে, কিছু জোরালো গ্রাহক আছেন যাঁদের সমালোচনায় সূক্ষ্মতার অভাব। যেহেতু এতে আরও সূক্ষ্ম একটি অ্যালগরিদম অপূরণীয়ভাবে পক্ষপাতদুষ্ট হয়ে পড়তে পারে, তাই তাঁদের চাহিদা মেটানোর জন্য আলাদা একটি অ্যালগরিদম রাখা হয়েছে। তাঁদের ডেটা সামলানোর জন্য আপনাকে কিছু সহায়ক ফাংশন দিতে বলা হয়েছে।
নিচের কাজগুলো সমাধানের ভিন্ন ভিন্ন উপায় থাকতে পারে, তবে প্রতিটি কাজ সর্বোচ্চ দুটি হায়ার-অর্ডার ফাংশন দিয়েই সমাধান করা যায়।
রেটিং সিস্টেমটি পাঁচ তারার ভিত্তিতে গড়া, যা সহজভাবে 1:5 ইন্টিজার নিয়ে গঠিত।
জোরালো গ্রাহকেরা কেবল 1 বা 5 রেটিং দেন, আর আমরা জানতে চাই কোনো গ্রাহক এই আচরণ দেখান কিনা।
all_15() তৈরি করুন, যা রেটিংয়ের একটি ভেক্টর নেয় এবং সব রেটিং 1 বা 5 হলে true রিটার্ন করে, আর অন্যথায় ফলস।
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() তৈরি করুন, যা রেটিংয়ের একটি ভেক্টর নেয়।
এখন পর্যন্ত ক্রমসঞ্চিত গড় রেটিংয়ের একটি ভেক্টর রিটার্ন করে।
ফলাফল দুই দশমিক স্থান পর্যন্ত রাউন্ড করুন।
আশা করি সংখ্যাগুলো বাড়তেই থাকবে, খুশি গ্রাহকদের জন্য!
satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
Exercism-এ সাইন আপ করুন, R ট্র্যাকের 21টি কনসেপ্ট111টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।