ফাংশনের সঙ্গে পরিচয় হয়েছিল আগেই বেসিকস কনসেপ্টে, এ রকম কিছু উদাহরণ দিয়ে:
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-এর মতোই।
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 আর বেস 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-এর কথা একটু ঘুরিয়ে বললে:
লুপ নিজে থেকে ধীর নয়, কিন্তু লুপের ভেতরে ধীর অপারেশন ঢুকিয়ে দেওয়াটা এরা বিপজ্জনকভাবে সহজ করে তোলে।
ভেক্টরাইজেশন বা হায়ার-অর্ডার ফাংশন আপনাকে এই ধরনের পারফরম্যান্স-ঘাতক থেকে রক্ষা করতে পারে।
আপনার কোম্পানি ওয়েবপেজ তৈরির জন্য ম্যাপিং ডেটা নিয়ে কাজ করে আসছে। কিছু আলোচনার পর সিদ্ধান্ত নেওয়া হয় যে, কিছু গণনা গতিশীলভাবে করতে R ব্যবহার শুরু করা হবে।
অনুগ্রহ করে সরাসরি ভেক্টর বিন্দুগুলো নিয়েই কাজ করার চেষ্টা করুন: সেগুলোকে আলাদা x ও y স্থানাঙ্কে ভেঙে ফেলার দরকার নেই।
scale(point, s) ফাংশনটি বাস্তবায়ন করুন, যা যেকোনো সংখ্যক মাত্রার একটি বিন্দু নেয় এবং পূর্বনির্ধারিত s দিয়ে সেটি স্কেল করে।
# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4
translate(point, ...) ফাংশনটি বাস্তবায়ন করুন, যা একটি নতুন বিন্দু রিটার্ন করে, যার প্রতিটি স্থানাঙ্ক ইলিপসিসে দেওয়া মান দিয়ে সরানো হয়।
এই ফাংশনটিকে যেকোনো মাত্রার বিন্দু সামলাতে সক্ষম হতে হবে, যেখানে ডট আর্গুমেন্টের সংখ্যা ওই মাত্রার সাথে মিলবে।
point2d <- c(2, 3)
# supply dx, dy translations
translate(point2d, 0.5, 0.6)
#> [1] 2.5 3.6
point3d <- c(2, 3, 4)
# supply dx, dy, dz translations
translate(point3d, 0.5, 0.6, 0.7)
#> [1] 2.5 3.6 4.7
আপনার কিছু সহকর্মীর R নিয়ে অভিজ্ঞতা কম, তাই আপনি {x, y} স্থানাঙ্ক জোড়ার জন্য পুনরায় ব্যবহারযোগ্য রূপান্তর তৈরিতে একটি ফাংশন ক্লোজার ব্যবহার করার সিদ্ধান্ত নেন।
transform2d(dx, dy, s) ফাংশনটি বাস্তবায়ন করুন, যা একটি ফাংশন রিটার্ন করে; সেই ফাংশনটি ক্লোজার ব্যবহার করে একটি বিন্দুর পুনরাবৃত্তিযোগ্য 2D স্থানান্তর ও স্কেলিং করে।
স্কেলিং আর্গুমেন্ট s ঐচ্ছিক হওয়া উচিত, ডিফল্ট মান 1.
রিটার্ন করা ফাংশনটি একটি 2D বিন্দু নেবে, তারপর:
dx ও dy দিয়ে এটি স্থানান্তর করবেs দিয়ে এটি স্কেল করবে# scale all coordinates by the same amount (2) in this example
# your code needs to be able to handle a vector `s`
tf2d <- transform2d(0.5, 0.6, 2)
class(tf2d)
#> [1] "function"
tf2d(c(2, 3))
#> [1] 5.0 7.2
# Accept default for `s`
tf2d <- transform2d(0.5, 0.6)
tf2d(c(2, 3))
#> 2.5 3.6
অপারেশনের ক্রম গুরুত্বপূর্ণ: আগে স্থানান্তর করে পরে স্কেল করলে সঠিক ফলাফল পাওয়া যায়, আগে স্কেল করে পরে স্থানান্তর করলে পাওয়া যায় না। প্রযুক্তিগত ভাষায় বললে, অপারেশনগুলো বিনিময়যোগ্য নয়।
ম্যাপিং ডেটায় উচ্চতাও থাকতে পারে, যা স্ক্রিনে শেডিং ও কনট্যুর যোগ করতে ব্যবহৃত হয়।
transform3d(dx, dy, dz, s) ফাংশনটি বাস্তবায়ন করুন, যা একটি ফাংশন রিটার্ন করে।
স্কেলিং আর্গুমেন্ট s ঐচ্ছিক হওয়া উচিত, ডিফল্ট মান 1.
রিটার্ন করা ফাংশনটি একটি 3D বিন্দু নেবে, পূর্বনির্ধারিত মান দিয়ে এটি স্থানান্তর করবে, তারপর s দিয়ে স্কেল করবে।
# using default `s`
tf3d <- transform3d(0.5, 0.6, 0.7)
point <- c(2, 3, 4)
tf3d(point)
#> [1] 2.5 3.6 4.7
# using vector `s`
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
tf3d(point)
#> [1] 1.25 2.16 3.29
Exercism-এ সাইন আপ করুন, R ট্র্যাকের 21টি কনসেপ্ট111টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।