برنامهنویسی تابعی چیست؟
برای هدف ما، اینها چند ویژگی کلیدی هستند:
با این حال، R یک زبان برنامهنویسی تابعی خالص نیست (برخلاف مثلاً Haskell).
به R بهعنوان یک زبان عملگرا نگاه کنید، که (روزافزون) قابلیتهای تابعی را در جایی فراهم میکند که راهی تمیز و آسان برای انجام کار هستند. بهطور کلی، تحلیل داده هدف است و برنامهنویسی تنها وسیلهای برای رسیدن به آن هدف است.
استفاده از «پایپ»های |> برای اتصال توابع، تکنیکی روزافزونرایج در چندین زبان است.
بهطور خلاصه:
برخی برنامهنویسان هنگام مرور کد، تلفظ پایپ را بهصورت «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"
این نوع پایپ در نسخهی 4.1.0 به R پایه اضافه شد و ممکن است ببینید که به آن «پایپ بومی» گفته میشود.
ویرایشگر 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 اسکالر را با recode_values() برداریشده از کتابخانهی dplyr جایگزین کنیم.
برداریسازی خوب است و 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 آشنا هستند و همچنان برای بردارها و لیستهای ساده بسیار محبوباند.
R پایه بسیاری توابع apply دیگر نیز دارد. فراهم کردن جایگزینی سادهتر و یکدستتر برای این نوع سردرگمی، هدفی کلیدی برای Tidyverse است.
اکنون بستهی purrr وجود دارد که چندین گزینهی اضافی مانند map() را فراهم میکند، در حالی که یکدستی در طراحی و شفافیت در پیامهای خطا را بهبود میبخشد.
این بسته که کمتر از 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 (بهعمد) سختگیرتر از R پایه است. اگر چیز خاصی بخواهید، توابع دقیقاً همان را برمیگردانند یا با پیام خطا متوقف میشوند.
سختگیرانه، اما در کاهش باگهای سختیافت بسیار مؤثر است.
توابع map بهطور کلی مقدار بازگشتی با همان طول ورودی دارند.
برخی توابع دیگر ابعاد داده را کاهش میدهند، مثلاً 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 توابع خودشان را دارند.
مفهوم توابع برداری دربارهی cumsum() و cumprod() در R پایه بحث کرد و کتابخانهی dplyr برای میانگینهای عددی cummean() و برای بردارهای منطقی cumall() و cumany() را دارد.
معمولاً میخواهیم عناصری از ورودی را که با یک محمول مطابقت دارند بگنجانیم (یا حذف کنیم).
در مفهوم فیلتر کردن بردار یک روش اصطلاحی برای انجام این کار دیدیم.
v <- 1:3
v[v >= 2]
#> [1] 2 3
این روش در R سنتی بسیار استفاده میشود، اما در خطوط پایپ تابعی مدرن خوب جا نمیافتد.
در نتیجه، purrr مجموعهای از توابع محمولی را فراهم میکند که از قرارداد معمول آرگومان f(input, func, ...) برای کار با پایپها پیروی میکنند.
نامها کمی با بیشتر زبانهای دیگر متفاوتاند (نامهایی مانند filter و drop در R معنای کاملاً متفاوتی دارند).
توابع متناظر در purrr شامل keep() و discard() و چند تای دیگر است.
همچنین every()، some() و none() را داریم تا جای all() و any() را در برگرداندن یک مقدار منطقی واحد بگیرند.
ما در حال راهاندازی یک باشگاه پنیر هستیم که از ML استفاده میکند تا بر اساس سابقه و سلیقهی مشتریان پنیردوستمان، پنیرهای تازهای را برای عرضه انتخاب کند.
اعضای جدید باید یک نظرسنجی اولیه را پر کنند تا ما بتوانیم دادههای پایهای را برای شروع جمعآوری کنیم. از این طریق مشخص شده است که زیرمجموعهای از مشتریان پرشور وجود دارند که در نقدهایشان ظرافت کمی دارند. چون این کار میتواند بهطور جبرانناپذیری باعث سوگیری یک الگوریتم دقیقتر شود، الگوریتم جداگانهای برای رسیدگی به نیازهای آنان در نظر گرفته شده است. از شما خواسته شده است چند تابع کمکی بنویسید تا دادههای آنان را سر و سامان دهید.
هرچند ممکن است راههای مختلفی برای حل تمرینهای زیر وجود داشته باشد، هرکدام را میتوان با حداکثر دو تابع مرتبهبالا حل کرد.
سیستم امتیازدهی بر پایهی پنج ستاره است که بهسادگی از اعداد صحیح 1:5 تشکیل شده است.
مشتریان پرشور فقط امتیاز 1 یا 5 میدهند و میخواهیم بدانیم آیا یک مشتری چنین رفتاری دارد یا نه.
تابع all_15() را پیادهسازی کنید که یک بردار از امتیازها میگیرد و اگر همهی امتیازها 1 یا 5 باشند true و در غیر این صورت false برمیگرداند.
all_15(c(2, 3, 4, 4, 1))
#> [1] FALSE
all_15(c(1, 5, 5, 1, 5))
#> [1] TRUE
باید اسم مشتری (یا شناسهی او) را به امتیازهایش نسبت دهیم.
تابع 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