البرمجة الوظيفية:سر قوة لغة R الخفية


نظري متقدم


هناك الكثير من المزايا التي تستفرد بها لغة الأحصاء العريقة R. فهي لغة سهلة التعلم (خصوصا لمن ليس لديه خلفية عميقة في علم الحاسب الآلي) و تتمتع بمجتمع حيوي وشغوف مما يثري قائمة الحزم والمكتبات المطورة من اجل هذه اللغة الجميلة. بالإضافة إلى انها صنعت خصيصا من أجل البيانات بأيدي مطورين ضليعين بالمشكلات التي يواجهها محللوا البيانات انفسهم. رغم كل ذلك الا أن هذه الأمور ليست ما يجعل لغة R فوق هرم اللغات في مجال علم البيانات. لغة R تتبنا في لبناتها الأساسية فلسفة برمجية عظيمة اسرت بها قلوب كثير من محللي البيانات وأنا منهم. هذه الفسلفة تسمى بالبرمجة الوظيفية وهي موضوع هذه التدوينة.

ما هي البرمجة الوظيفية Functional Programming ؟

البرمجة الوظيفية هي احد انماط البرمجة كالبرمجة الشيئية والبرمجة الحتمية. في عالم البرمجة الوظيفية, الدالة هي المواطن الأول حيث يتم التعامل معها كأي متغير نألف التعامل معه. فبالإمكان تخزين الدوال في المتغيرات وتمريرها كمتغيرات لدوال أخرى. كذلك يمكن انشاءها داخل الدوال مما يتيح الكثير من الأمكانيات المفيدة جدًا في مجال علم البيانات.

مبادئ البرمجة الوظيفية

هذه الفلسفة تقوم بشكل عام على ثلاث مبادئ

1 - إمكانية الدوال عالية المستوى

في لغة R يمكن ان نقوم بتعريف دالة مصنعة لدوال اخرى حسب احتياجاتنا. هذه الفكرة ليست دخيلة في علم الرياضيات. مثلًا, دالة الأشتقاق تقوم بأخذ دالة وترجع دالة أخرى (الدالة المشتقة). لذلك تعتبر دالة الأشتقاق دالة عالية المستوى لأن مدخلاتها دوال ومخرجاتها ايضا دوال.

2 - الدوال يجب ان تكون نقية

الدوال النقية او ما تعرف بـ pure functions تمتاز بأنها لا تغير حال مدخلاتها من المتغيرات بل تنشأ متغيرات جديدة. كذلك لا يوجد آثار جانبية من تشغيلها كتغيير متغيرات خارج نطاق الدالة. سوف نتطرق لاحقا لأمثلة واكواد توضح ما نقصده بتلك الآثار.

3 - استبدال حقات التكرار (loops) بالدوال الوظيفية

حلقات التكرار يشوبها الكثير المشاكل مثل الكفاءة والسرعة وايضا التكرار في الأكواد مما يجعل البرمجة اكثر عرضة للأخطاء. البرمجة الوظيفية تهدف لحل هذه المشاكل بطريقة جميلة ومختصرة.

مقارنة فلسفة البرمجة الوظيفية بالفلسفات الأخرى

في البرمجة الحتمية او ما تعرف بالـ imperative programming تغير العمليات بشكل تسلسلي حالة البرنامج او البيانات. هذه الطريقة تجعل تصحيح اخطاء الأكواد عملية صعبة حيث أن حال المتغيرات تعتمد على متى وأين تم تغييرها في الكود على خلاف البرمجة الوظيفية. في البرمجة الوظيفية, لا تقوم الدوال بتغيير حال المتغيرات بل تعرف متغيرات جديدة تحمل هذه التغيرات.

// In C++ Language
#include <Rcpp.h>
int x = 0; 
// [[Rcpp::export]]
int accumulate(int num) {
  x += num;
  return x;
}
# نتائج هذا الكود يعتمد على مكان تنفيذ الأمر مما يجعل تقفي اثر المشكلة عملية صعبة
accumulate(1)
## [1] 1
accumulate(1)
## [1] 2
accumulate(1)
## [1] 3

يتشابه الأمر في فلسفة البرمجة الشيئية. حيث يعتمد تغير خصائص الأشياء أو الكائنات على نوع العمليات التي أُجريت عليها. ليس ذلك فحسب بل يمكن ايضا تغيير متغيرات النوعية او الصنفية لتلك الكائنات مما ينتج عنه آثار جانبية على جميع الكائنات الموجودة.

# In Python Language
class human:
    legs = 2
    def __init__(self, name, age):
        self.name = name
        self.age = age

hussain = human("Hussain", 32)
mohammed = human("Mohammed", 29)

# عدد ارجل اي أنسان حسب تعريف صنف الأنسان اعلاه هو 2
hussain.legs
## 2
mohammed.legs 
## 2
# In Python Language

# لكن لو تم تغيير هذه الخاصية في صنف الأنسان , سوف يكون هناك اثار جانبية اخرى على كل الكائنات الموجودة 
human.legs = 4
hussain.legs
## 4
mohammed.legs
## 4

روعة البرمجة الوظيفية في لغة R

سنرى الآن مدى روعة البرمجة الوظيفية من خلال ثلاث امثلة تجسد القوة الخارقة لهذه اللغة

المثال الأول : الحسابات الإحصائية الوصفية للبيانات

من المهام المتكررة لعلماء البيانات هي حساب بعض الأحصائيات الوصفية لأعمدة البيانات. الطريقة البدائية هي ان نقوم بحساب عدد الأعمدة ثم نقوم بتطوير حلقة تكرار تقوم بنفس الحسابات لكل عامود. يتطلب الأمر ايضا ان نعرف متغير يقوم بتجميع تلك الحسابات. سيكون الكود مشابه للتالي

summary_stat <- function(df) { 
  n_col <- ncol(df)
  funs <- c("mean", "median", "sd", "IQR")
  statistics <- matrix(nrow = 4, ncol = n_col,dimnames = list(funs))
  for (i in seq(n_col)){
    for (j in seq_along(funs)){
      statistics[j,i] <- round(do.call(funs[j],list(x = df[,i])),2)
    }
  }
  colnames(statistics) <- colnames(df)
  return(statistics)
}

summary_stat(cars)
##        speed  dist
## mean   15.40 42.98
## median 15.00 36.00
## sd      5.29 25.77
## IQR     7.00 30.00

لكن بالإمكان الإستعانة بالبرمجة الوظيفية والحصول على نفس النتائج بدون معرفة عدد الأعمدة او تطوير حلقة تكرار كما هو في الكود التالي

summary_stat <- function(column){
    funs <- c(mean, median, sd, IQR)
    lapply(funs, function(f) round(f(column,na.rm = TRUE),2))
}
# هنا تقوم الدالة sapply بتطبيق الدالة التي قمنا بتعريفها على جميع الأعمدة بدون اي حلقات تكرار او معرفة مسبقة بعدد الأعمدة 
sapply(cars, summary_stat)
##      speed dist 
## [1,] 15.4  42.98
## [2,] 15    36   
## [3,] 5.29  25.77
## [4,] 7     30

المثال الثاني: تبسيط متعددة الحدود

لعلك تتذكر عندما كنت على مقاعد الثانوية العامة في حصص الرياضيات طريقة تبسيط متعددة الحدود. دعني انعش ذاكرتك بهذا المثال. لنفترض ان لدينا هذه المعادلة.

\[ (x + y)^2 \]

نقوم بتبسيطها عن طريق تطبيق ما حفظناه عن المعادلات التربيعية وهو " تربيع الأول زائد اثنان ضرب الأول والثاني زائد تربيع الثاني" لينتج لنا هذه المعادلة.

\[ x^2 + 2 * x*y + y^2 \]

لكن ماذا لو امكننا تطوير دالة تكون مدخلاتها درجة متعددة الحدود ومخرجاتها دالة مبسطة. هذه الطريقة لا تمكننا من تبسيط المعادلة بل أستخدامها كأي معادلة اخرى. بالفعل هذه الدالة ليست نظرية فقط بل قمت بتطويرها و استخدامها في حزمة bezieR المتخصصة لتحليل منحنيات بيزيه. يمكنك قراءة التدوينة الخاصة بها هنا.

make_terms <- function (n) {
  n <- n + 1
  cp <- rep(1,n)
  # Pascal trinagle
  # this is a pre-calculated terms for optimization purposes to reducing the factorial operation
  lut <- list(c(1), # n : 0
              c(1,1), # n : 1
              c(1,2,1), # n : 2
              c(1,3,3,1), # n : 3
              c(1,4,6,4,1), # n : 4
              c(1,5,10,10,5,1), # n : 5
              c(1,6,15,20,15,6,1)) # n : 6
  
    if(missing(cp) || length(cp)!= n )
    {
      stop(paste0("you must provide number of terms with their control points coordinates"))
    }
  
  if(n > 6) lut[[n]] <- choose(n-1,0:(n-1))
  
  trms <- rep(NA, n)
  for (i in 1:n) {
    trms[i] <- paste0(cp[i],"*",lut[[n]][i],"*","x^",(n-i), "*y^",i-1," ")
  }
  eq_str <- paste0(trms,collapse = "+")
  
  function(x,y,equation = eq_str){
    if(!missing(x) && !missing(y)){
      expr <- str2lang(eq_str)
      eval(expr)
    } else {
      print(equation)
    }
  }
}

الكود اعلاه عبارة عن دالة عالية المستوى تقوم بإنشاء دالة مبسطة . فلو افترضنا اننا نريد ان نبسط المعادلة السابقة وهي معادلة تربيعية فبالإمكان صناعة هذه المعادلة بكل سهولة عن طريق الكود التالي

eq_2 <- make_terms(2)
eq_2()
## [1] "1*1*x^2*y^0 +1*2*x^1*y^1 +1*1*x^0*y^2 "
# كما يمكننا التعويض لقيم المتغيرات اكس و واي وإيجاد الناتج بكل سهولة
eq_2(x = 2, y = 3)
## [1] 25
# يمكنك التحقق من النتيجة بنفسك 

المثال الثالث: الإشتقاق

كما هو في المثال السابق, يمكننا ايضا تطوير دالة تكون مدخلاتها اي معادلة ومخرجاتها دالة اخرى تكون اشتقاق للدالة المدخلة. هذه القوة في البرمجة ما يميز لغة R عن الكثير من اللغات البرمجية.

library("mosaicCalc")

# لنقم بإشتقاق دالة بسيطة 
D(x^3 + x^2 + x ~ x)
## function (x) 
## 3 * x^2 + 2 * x + 1
dx <- D(x^3 + x^2 + x ~ x)
dx(x = 2) 
## [1] 17

ختام

البرمجة الوظيفية هي نمط رائع جدا من البرمجة يفتح آفاق واسعة لتطبيقات قيمة في مجال علم البيانات. الكثير من اللغات البرمجية المعاصرة بدأت بتبني هذه الفلسفة ومحاولة تمكينها بطريقة جذابة للمبرمجين والمحللين. من وجهة نظري الشخصية, لغة R في مقدمة الركب في هذا المجال مما يكسبها مكانة خاصة لدي ولدى كثير من المحللين.


جرب بنفسك

كامل الكود تجده هنا

comments powered by Disqus