ما هي البرمجة الوظيفية؟
في سياقنا هذا، هذه بعض السمات الأساسية:
لكن R ليست لغة وظيفية نقية (على عكس Haskell مثلًا).
انظر إلى 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 في الإصدار 4.1.0، وقد تراه مذكورًا باسم «الأنبوب الأصلي».
يستخدم محرر RStudio (من بين محررات أخرى) Ctrl-Shift-M كاختصار لوحة مفاتيح لإضافة أنبوب مع مسافات مناسبة.
apply
رأينا سابقًا أن كثيرًا من الدوال في 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()صُممت «التطبيق على القوائم» للعمل على مدخلات من القوائم، لكن المتجهات ستُحوَّل بصمت إلى قوائم عند الحاجة.
الناتج قائمة، لذا قد يحتاج إلى فكّ للحصول على متجه:
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()ستفعل «التطبيق المبسّط» شيئًا مشابهًا لـ 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 أيضًا على العديد من دوال apply الأخرى.
ويعد توفير بديل أبسط وأكثر اتساقًا لهذا النوع من الالتباس هدفًا رئيسيًا لـ Tidyverse.
توجد الآن حزمة purrr، التي توفّر عدة خيارات إضافية مثل map()، مع تحسين الاتساق في التصميم والوضوح في رسائل الأخطاء.
ولأن هذه الحزمة أقل تقيّدًا بالتوافق مع الإصدارات السابقة مقارنة بـ Base R، فإنها تقرّب R كثيرًا من البرمجة الوظيفية الحديثة كما هي موجودة في لغات أخرى.
تحتوي purrr على دوال كثيرة ومتنوعة يصعب تلخيصها بإيجاز.
وتشمل المصادر التي تساعد على التنقل في المكتبة:
map المتعددةتُطبّق دالة map() دالةً على كل عنصر في المدخل، وتُرجع دائمًا قائمة.
حتى الآن، يبدو هذا مثل lapply()، لكن باسم جديد، وربما مع رسائل أخطاء أفضل.
لكن map() ليست سوى عضو واحد من عائلة كبيرة.
بعض الأسئلة التي تستحق التفكير عند اختيار دالة:
map() دائمًا قائمة، لكن إذا أردت نوعًا معينًا من المتجهات الذرية فاستخدم map_chr() للسلاسل النصية، وmap_int() للأعداد الصحيحة، وهكذا.i في بداية أسماء الدوال، مثل imap().map بـ map2 في الاسم، كما في map2_dbl() لمدخلين (مثل القيم والأوزان، لحساب متوسط مرجّح)pmap لقائمة من المدخلات المتوازية، كما في pmap_chr(). ونعم، التوازي على مستوى العتاد ممكن.map_if() لتطبيق مرشّح تحقّق (منطقي)، وmap_at() لتحديد معايير على الفهارس الموضعية، وhead_while() لمعالجة المدخل حتى يفشل عنصر في تحقّق.modify() بدلًا من map، أو modify_tree() للتطبيق التعاودي.تأخذ هذه الدوال بيانات المدخل كوسيط أول كلما أمكن، لضمان عملها جيدًا مع الأنابيب.
كذلك، فإن purrr أكثر تدقيقًا (عن قصد) من Base R. إذا طلبت شيئًا محددًا، فستُرجع الدوال ذلك بالضبط أو تتوقف برسالة خطأ.
قسوة، لكنها فعّالة جدًا في تقليل الـ Bug التي يصعب تتبعها.
عادةً ما تكون القيمة المُرجَعة لدوال map بالطول نفسه الذي للمدخل.
تختزل بعض الدوال الأخرى أبعاد البيانات، مثل 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 دوالها الخاصة.
ناقش مفهوم دوال المتجهات الدالتين cumsum() وcumprod() في Base 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() في إرجاع قيمة منطقية واحدة.
نحن نؤسّس ناديًا للجبن، سيستخدم تعلّم الآلة لاختيار أجبان جديدة نقدّمها لعملائنا من محبّي الجبن، بناءً على سجلاتهم وأذواقهم.
يُطلب من الأعضاء الجدد ملء استبيان أوّلي حتى نجمع بعض البيانات الأساسية لنبدأ بها. ومن خلال ذلك، تبيّن وجود فئة من العملاء المتحمسين يفتقرون إلى الفروق الدقيقة في تقييماتهم. ولأن هذا قد ينتهي إلى تحييز خوارزمية أكثر دقة بشكل لا رجعة فيه، فقد أُنشئت خوارزمية منفصلة لتلبية احتياجاتهم. والمطلوب منك تقديم بعض الدوال المساعدة للتعامل مع بياناتهم.
قد توجد أساليب مختلفة لحل المهام التالية، لكن يمكن حل كل منها بما لا يزيد عن دالتين من الرتبة الأعلى.
يقوم نظام التقييم على خمس نجوم، ويتكون ببساطة من الأعداد الصحيحة 1:5.
لا يمنح العملاء المتحمسون سوى تقييمات 1 أو 5، ونريد أن نعرف ما إذا كان عميل ما يُظهر هذا السلوك.
نفّذ الدالة all_15() التي تأخذ متجهًا من التقييمات وتُرجع true إذا كانت جميع التقييمات 1 أو 5، وتُرجع خطأ فيما عدا ذلك.
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
سجّل في Exercism لتتعلّم وتتقن R عبر 21 مفهومًا111 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.