ما هي البرمجة الدالية؟
هذا سؤال كبير يستحق إجابة طويلة (ويكيبيديا تقدم واحدة، وAdvanced R يقدم أخرى).
بالنسبة لأغراضنا، هذه بعض السمات الأساسية:
لكن R ليست لغة دالية خالصة (على عكس Haskell مثلًا).
انظر إلى R كلغة عملية، توفر (بشكل متزايد) إمكانات دالية عندما تكون هذه الإمكانات أسلوبًا نظيفًا وسهلًا لإنجاز المهمة. وعمومًا، تحليل البيانات هو الهدف، والبرمجة مجرد وسيلة لتحقيقه.
استخدام الأنابيب |> لربط الدوال أسلوب شائع بشكل متزايد في عدة لغات.
باختصار:
يجد بعض المبرمجين أنه من المفيد نطق الأنبوب "then" عند مراجعة الكود: نفّذ الخطوة 1 THEN نفّذ الخطوة 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"
أُضيف هذا النوع من الأنابيب إلى 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() للعمل عليها.
وبشكل أكثر تقنية، يسميها المبرمجون غالبًا "الدوال عالية الرتبة" ويسميها علماء الرياضيات "functionals"، لأنها تأخذ دوال كوسائط.
في الوقت الحالي، تأمّل 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 الأساسية أيضًا عدة دوال تطبيق أخرى:
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() لمعالجة المدخل إلى أن يخفق عنصر في تحقق ما.modify() بدلًا من map، أو modify_tree() للتطبيق العودي.تأخذ هذه الدوال بيانات المدخل كوسيط أول كلما أمكن، لضمان عملها بشكل جيد مع الأنابيب.
كما أن purrr أكثر تدقيقًا (عن قصد) من R الأساسية. فإذا طلبت شيئًا محددًا، ستُرجع الدوال ذلك بالضبط أو تتوقف برسالة خطأ.
قاسٍ، لكنه فعال جدًا في تقليل الـ bugs التي يصعب اكتشافها.
عادةً ما تكون قيمة إرجاع دوال 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
لبعض الأنواع الشائعة من التراكم دوال خاصة بها.
ناقش مفهوم دوال المتجهات الدالتين 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() في إرجاع قيمة منطقية واحدة.
تستخدم معظم اللغات الدالية العودية كبديلها المفضل عن الحلقات.
وتمتلك R خيارات أخرى، وغالبًا أفضل: الدوال المتجهية والدوال عالية الرتبة.
لكن العودية مدعومة في R، وقد تكون مفيدة في سياقات مثل التنقل في الهياكل الشجرية.
عمق العودية الأقصى هو 5000 افتراضيًا، وهو سخي نسبيًا (القيمة الافتراضية في Python هي 1000). ويمكن تجاوز القيمة الافتراضية.
تحسين استدعاء الذيل ليس معيارًا في 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، وهي مفيدة أحيانًا، لكن غالبًا ما توجد أساليب أبسط.