مفهوم الدوال


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

بناء الدوال


يمكنك تعريف أي دالة في لغة الأسس بالصيغة التالية:

عرف اسم_الدالة: دالة (معطيات): صنف_الإرجاع {
  // متن الدالة
}
def function_name: function (parameters) => return_type {
  // function body
}

أو بالصيغة المختصرة:

  دالة اسم_الدالة (معطيات): صنف_الإرجاع {
    // متن الدالة
  }
function function_name (parameters): return_type {
  // function body
}

حيث أن:

  • اسم_الدالة (function_name): اسم تستدعي الدالة به.
  • معطيات (parameters): الوسطاء أو المعطيات التي يتم تمريرها إلى الدالة.
  • صنف_الإرجاع (return_type): تحدد نمط البيانات الذي سترجعه الدالة عندما تنتهي من التنفيذ. ونوع الإرجاع في الدالة يمكن أن يكون أي نوع من أنواع البيانات الموجودة في الأسس ( صحيح - عائم - محرف - إلخ.. ). ويمكن وضع اسم صنف مستخدم (class) معين، و هنا يكون القصد أن الدالة ترجع كائن من هذا الصنف (سنرى ذلك في دروس لاحقة). في حال كانت الدالة لا ترجع أي قيمة، لانقوم بوضع أي شيئ مكان صنف الإرجاع والنقطتين التي قبله، كما سنرى في المثال التالي.
  • متن_الدالة (function body): تعني جسم الدالة، و المقصود بها الأوامر التي نضعها في الدالة.

المثال الأول:

في المثال التالي سنقوم بتعريف دالة تقوم بطباعة عبارة معينة، وسنسمي هذه الدالة myFunction، ولن نقوم بكتابة أي نمط إرجاع لأن الدالة تقوم فقط بعملية طباعة، بعد ذلك سنقوم باستدعاء هذه الدالة.

اشمل "مـتم/طـرفية"؛
استخدم مـتم.طـرفية؛
دالة دالتي () {
  اطبع("دالتي الأولى قد استدعيت")؛
}
دالتي()؛
/*
دالتي الأولى قد استدعيت
*/
import "Srl/Console.alusus"
use Srl.Console;
function myFunction () {
  print("My first function is called");
}
myFunction();
/*
My first function is called
*/

المثال الثاني:

في المثال التالي سنقوم بتعريف دالة تقوم بحساب مجموع عددين مُدخلين، ثم تقوم بإرجاع النتيجة.

اشمل "مـتم/طـرفية"؛
استخدم مـتم.طـرفية؛
عرف ا: صـحيح = 3؛
عرف ب: صـحيح = 14؛
دالة مجموع (س: صـحيح، ص: صـحيح): صـحيح {
  أرجع س + ص؛
}
عرف ت: صـحيح = مجموع(ا، ب)؛
اطبع("%d"، ت)؛ // 17
import "Srl/Console.alusus";
use Srl.Console;
def a: Int = 3;
def b: Int = 14;
function sum (x: Int,y: Int): Int {
  return x + y;
}
def z: Int = sum(a, b);
print("%d", z); // 17

المترجم يقرأ الكود سطراً سطراً مع تنفيذ الأوامر الموضوعة في كل سطر بشكل مباشر عندما يتم تشغيل البرنامج. لهذا السبب يجب دائماً أن تكون الدالة التي تريد استدعاءها معرّفة سابقاً حتى لا يظهر لك مشكلة عند تشغيل البرنامج. لكن ذلك ينطبق على المجال الخارجي فقط (الجذر)، أي لا يعني أنك دائماً تحتاج لكتابة الدوال بتسلسل معين. مثلا المثال التالي سيظهر خطأ:

دالة د1() {
  د2()؛
}
د1()؛
دالة د2() {
}
func f1() {
  f2();
}
f1();
func f2() {
}

بينما المثال التالي صحيح:

دالة د1() {
  د2()؛
}
دالة د2() {
}
د1()؛
func f1() {
  f2();
}
func f2() {
}
f1();

والسبب أننا في الحالة الأولى استدعينا الدالة د1 (f1) التي تحوي داخلها استدعاءاً للدالة د2 (f2) الغير معرّفة بعد، أما في الحالة الثانية تجنبنا ذلك من خلال تعريفها ثم استدعاء الدالة د1 (f1).

أثناء استدعاء دالة، يمكن تمرير المعطيات إلى الدالة بطريقتين، استدعاء حسب القيمة واستدعاء بالمرجع، وفي الأمثلة السابقة كنا نستخدم الطريقة الأولى. والفرق الأساسي بين الطريقتين يتجلى في مايلي:

  1. في الطريقة الأولى (الاستدعاء بالقيمة) يتم تمرير قيمة المتغير الفعلية إلى الدالة، ومنطقة الذاكرة الجديدة التي تم إنشاؤها للمعطيات التي تم تمريرها، يمكن استخدامها فقط داخل الدالة، ولا يمكن تعديل المعطيات الفعلية هنا (لأنه تم إنشاء نسخة مستقلة عنها) أي التعديلات تبقى محصورة داخل حدود الدالة. لاحظ المثال التالي:
    اشمل "مـتم/طـرفية"؛
    استخدم مـتم.طـرفية؛
    عرف ا: صـحيح = 3؛
    عرف ب: صـحيح = 14؛
    دالة مجموع (ا: صـحيح، ب: صـحيح): صـحيح {
      ا += 1؛
      أرجع ا + ب؛
    }
    عرف ت: صـحيح = مجموع(ا، ب)؛
    اطبع("%d\ج"، ت)؛ // 18
    اطبع("%d\ج"، ا)؛ // 3؛
    // لاحظ أن قيمة المتغير الأصلي لم تتأثر بالتعديل
    
    import "Srl/Console.alusus";
    use Srl.Console;
    def a: Int = 3;
    def b: Int = 14;
    function sum (a: Int, b: Int): Int {
      a += 1;
      return a + b;
    }
    def z: Int = sum(a, b);
    print("%d\n", z); // 18
    print("%d\n", a); // 3
    // لاحظ أن قيمة المتغير الأصلي لم تتأثر بالتعديل
    
  2. في الطريقة الثانية، بدلاً من نسخ المتغير يتم تمرير عنوانه كمعطى للدالة، ويتم استخدام المؤثر ptr~ لتمرير عنوان المتغير إلى الدالة عند استدعاءها، والتغييرات في الدالة على المعطيات تؤدي إلى تغيير المتغيرات الأصلية (لأننا نعمل على نفس الموقع الذاكري).
    في المثال التالي سنقوم بإعادة كتابة المثال السابق لكن مع استخدام الاستدعاء بالمرجع، كما سنقوم بتعديل قيمة المتغير a داخل الدالة لنرى كيف سينتقل هذا التغيير خارج حدود الدالة أيضاً.
    اشمل "مـتم/طـرفية"؛
    استخدم مـتم.طـرفية؛
    عرف ا: صـحيح = 3؛
    عرف ب: صـحيح = 14؛
    دالة مجموع (ا: مؤشر[صـحيح]، ب: مؤشر[صـحيح]): صـحيح {
      ا~محتوى += 1؛
      أرجع ا~محتوى + ب~محتوى؛
    }
    عرف ت: صـحيح = مجموع(ا~مؤشر، ب~مؤشر)؛
    اطبع("%d\ج"، ت)؛ // 18
    اطبع("%d\ج"، ا)؛ // 4؛
    
    import "Srl/Console.alusus"
    use Srl.Console;
    def a: Int = 3;
    def b: Int = 14;
    function sum (a: ptr[Int], b: ptr[Int]): Int {
      a~cnt += 1;
      return a~cnt + b~cnt;
    }
    def z: Int = sum(a~ptr, b~ptr);
    print("%d\n", z); // 18
    print("%d\n", a); // 4
    

    لاحظ هنا كيف أن التغيرات طالت المتغير الأساسي لأننا لم نقم بإنشاء نسخة عنه وإنما نتعامل معه مباشرة من خلال معرفة عنوانه المرجعي. كما يمكنك استخدام السندات بدلاً من المؤشرات كما يلي:

    اشمل "مـتم/طـرفية"؛
    استخدم مـتم.طـرفية؛
    عرف ا: صـحيح = 3؛
    عرف ب: صـحيح = 14؛
    دالة مجموع (ا: سند[صـحيح]، ب: سند[صـحيح]): صـحيح {
      ا += 1؛
      أرجع ا + ب؛
    }
    عرف ت: صـحيح = مجموع(ا، ب)؛
    اطبع("%d\ج"، ت)؛ // 18
    اطبع("%d\ج"، ا)؛ // 4؛
    
    import "Srl/Console.alusus"
    use Srl.Console;
    def a: Int = 3;
    def b: Int = 14;
    function sum (a: ref[Int], b: ref[Int]): Int {
      a += 1;
      return a + b;
    }
    def z: Int = sum(a, b);
    print("%d\n", z); // 18
    print("%d\n", a); // 4
    

الدوال المرنة


هي دوال يمكن أن تأخذ عدداً متغيراً من الوسائط (وبأصناف غير محددة)، مما يضيف المرونة لبرنامجك. ولفهم هذه المرونة، سنبدأ بالمثال التالي.

إذا أردنا كتابة دالة تقوم بإضافة عددين صحيحين، فيمكننا كتابة دالة بالشكل التالي:

دالة اجمع_رقمين (الرقم_الأول: صـحيح، الرقم_الثاني: صـحيح): صـحيح {
  أرجع الرقم_الأول + الرقم_الثاني؛
}
function addNumbers (nNumberOne: Int, nNumberTwo: Int): Int {
  return nNumberOne + nNumberTwo;
}

وإذا أردنا كتابة دالة أخرى في نفس البرنامج تقوم بإضافة 3 أعداد صحيحة، فيمكننا كتابة:

دالة اجمع_أرقام (الرقم_الأول: صحيح، الرقم_الثاني: صحيح، الرقم_الثالث: صحيح): صحيح {
  أرجع الرقم_الأول + الرقم_الثاني + الرقم_الثالث؛
}
function addNumbers (nNumberOne:int, nNumberTwo:int, nNumberThree:int): int {
  return nNumberOne + nNumberTwo + nNumberThree;
}

وبتزايد عدد الأرقام التي نحتاج إلى إضافتها، يمكننا الاستمرار في إضافة المزيد من الدوال مع العدد المناسب من الوسائط التي تمثل العدد الإجمالي للأرقام التي نود إضافتها.

لكن، قد يصبح ذلك مرهقاً خصوصاً إذا تطلب الأمر كتابة دوال أخرى لإضافة 4 أعداد و 5 أعداد إلخ.. أو إذا لم تكن تعرف عدد الأعداد التي تحتاج إلى إضافتها (ربما 5 أو 10 أو 100) وربما سيزداد الأمر تعقيداً إذا تطلب الأمر دوالاً بأصناف مختلفة، على سبيل المثال دالة تقوم بإضافة عدد غير محدد من الأعداد غير محددة الصنف (قد تكون صحيحة أو حقيقية)، وهلم جراً على هذا المنوال..

لحسن الحظ فإن لغة الأسس تعطيك حلاً سحرياً لذلك، وهو الدوال بمعطيات مرنة (الدوال المرنة).

- لابد أنك لاحظت في الدالة `اطبع` (print) عندما تريد طباعة رقم واحد، نقوم بشيء من هذا القبيل.

اطبع("الرقم الأول = %d"، الرقم_الأول)؛
print("the one number = %d", nOneNumber);

عندما تريد طباعة رقمين نستخدم نفس الدالة كما هو موضح أدناه:

اطبع("الرقم الأول: %ي، الرقم الثاني: %d"، الرقم_الأول، الرقم_الثاني)؛
print("the first number = %d, the second number =%d", nOneNumber, nSecondNumber);

هذا لأن الدالة `اطبع` هي دالة مرنة في لغة الأسس، ويمكنها استقبال أي عدد من المعطيات.

تحديد المعطيات كمعطيات مرنة يتم باستخدام المؤثر ... عند تعريف صنف المعطى. أي أن تسبق صنف المعطى بـ ... يجعل ذلك المعطى مرناً ما يمكن المستخدم من تمرير عدد غير محدد من ذلك الصنف.
لتعريف دالة بمعطيات مرنة يمكنك استخدام الصيغة التالية:

دالة اسم_الدالة (ا: صـنف_المعطى_ا، معطيات: ...صـنف_المعطيات_ب) { ... }
function function_name (a:Data_type_a, arg_group_name: ...Data_type_b) { ... }

حيث أن:

  • ا أو a مُعطى عادي.
  • `معطيات` (arg_group_name) هو اسم لمجموعة المعطيات المرنة.
  • `صـنف_المعطى_ا` و `صـنف_المعطيات_ب` (DataType_a و Data_type_b) هما صنفي المعطى الأول والعمطيات المرنة على التوالي.

استبدل `صـنف_المعطيات_ب` (Data_type_b) بالكلمة المفتاحية `أيما` (any) في حال كان صنف المعطيات المرنة غير محدد.
يمكن في الأسس تحديد عدد أدنى وأعلى من المعطيات، كما يلي:

  دالة اسم_الدالة (
    ا: صـنف_المعطى، معطيات: ...[صـنف_المعطيات، الحد_الأدنى، الحد_الأعلى]
  ) { ... }
function function_name (
  a1:Data_type ,arg_group_name: ...[Data_type, min_count, max_count]
) { ... }

أي أن عدد المعطيات لا يمكن أن يقل عن `الحد_الأدنى` (min_count) ولا أن يزيد عن `الحد_الأعلى` (max_count).

استدعاء دالة مرنة المعطيات يتم بنفس طريقة استدعاء أي دالة أخرى.

استخدام المعطيات المرنة داخل الدالة يتم من خلال المؤثر `~المعطى_التالي` (~next_arg) بالشكل التالي:

معطيات~المعطى_التالي[صـنف_المعطى]
arg_group_name~next_arg[Data_type]

حيث أن المؤثر يحتاج صنف المعطى لأن التعريف قد لا يحدد صنفاً للمعطيات (أي عندما تحدد صنف المعطى لمجموعة المعطيات ب `أيما` (any)) وبالتالي يحتاج المستخدم أن يحدد الصنف بنفسه اعتماداً على معطيات أخرى مثل سلسلة محارف تحدد بنية المعطيات المرنة كما هو الحال مع دالة `اطبع` (print) (تكافئ الدالة printf في السي). من المهم الملاحظة أن كل استخدام للمؤثر `~المعطى_التالي` (~next_arg) يسحب عنصراً من مجموعة المعطيات، أي أن الولوج للمعطيات يتم بشكل تسلسلي ولا يمكن الولوج لنفس العنصر عدة مرات أو الولوج بشكل عشوائي. كما أن تحديد عدد العناصر والوقوف بعد سحب آخر عنصر مسؤولية المبرمج حيث أن الأسس لا تملك طريقة تعرف بها عدد العناصر المتبقية، ولذلك يحتاج المستخدم لإضافة عدد العناصر كمعطى أولي في الدالة.

المثال الأول:
سنقوم الآن بتعريف دالة مرنة تقوم بإضافة عدد غير محدد من الأعداد الصحيحة.
لاحظ أننا هنا سنقوم بتعريف دالة تقوم بالتعامل مع صنف واحد معروف وهو النمط `صحيح` (int).

اشمل "مـتم/طـرفية"؛
استخدم مـتم.طـرفية؛
دالة اجمع_الأرقام (عدد: صحيح، الأرقام: ...صحيح): صحيح {
  عرف مجموع: صحيح = 0؛ // تعريف متغير لجمع الأعداد المُراد إضافتها
  عرف ع: صحيح؛
  لكل ع = 0، ع < عدد، ++ع {
    مجموع += الأرقام~المعطى_التالي[صحيح]؛ // استخراج العناصر وإضافتها للمجموع
  }
  أرجع مجموع؛
}
عرف رقم: صحيح = 5؛
رقم += اجمع_الأرقام(4، 3، 2، 5، 10)؛
اطبع("%d\ج"، رقم)؛ // 25
رقم += اجمع_الأرقام(1، 300)؛
اطبع("%d\ج"، رقم)؛ // 325
import "Srl/Console.alusus";
use Srl.Console;
function addNumbers (count:int, nNumber: ...int): int {
  def sum: int = 0; // تعريف متغير لجمع الأعداد المُراد إضافتها
  def i: int;
  for i = 0, i < count, i++ {
    sum += nNumber~next_arg[int]; // استخراج العناصر وإضافتها للمجموع
  }
  return sum;
}
def num: int = 5;
num += addNumbers(4, 3, 2, 5, 10);
print("%d\n", num); // 25
num += addNumbers(1, 300);
print("%d\n", num); // 325

المثال الثاني:
المثال التالي يُعرّف دالة مرنة تقوم باستقبال عدد غير محدد من المُعطيات من الصنفين `نـص` (String) أو `صحيح` (int) وتقوم بطباعتها.
لاحظ أنه في المثال السابق كان صنف البيانات معروفاً كما ذكرناً، لكن هنا صنف البيانات غير محدد، وبالتالي لابد من تحديد طريقة لمعرفة صنف البيانات، وهنا سنستخدم سلسلة محارف، أي بشكل مشابه لدالة `اطبع` (print) التي كنا نحدد لها الرمز d للإشارة إلى عدد صحيح أو f لعدد حقيقي أو c لمحرف. هنا سنستخدم المحرف # للإشارة إلى عدد صحيح والمحرف $ للإشارة إلى متغير من صنف `نـص` (String).

قبل البدء لابد من ذكر الملاحظات التالية (تم ذكرها في دروس سابقة):

  1. أي سلسلة محارف يتم تخزينها في الذاكرة بشكل متسلسل، كما ويضاف إلى نهايتها المحرف 0\ (تتم إضافتها في الذاكرة أي بشكل مخفي عنك لتدل على نهايتها). أي في حال كان لدينا سلسلة المحارف التالية:
    "#$#"

    فإن تمثيلها بالذاكرة يكون:

    {'#','$','#','\0'}

  2. في حالة قمنا بتعريف مؤشر على محرف:
    عرف م: مؤشر[مـحرف]؛
    
    def p: ptr[Char]
    

    وجعلناه يشير إلى أول محرف في السلسلة التالية:

    "#$#"

    ثم قمنا بإضافة 1 إلى المؤشر:

    م = م + 1
    p = p + 1

    فإن ذلك يكافئ الانتقال بمقدار 1 بايت في الذاكرة لأن نمط المؤشر هو char، وبالتالي هذا سيجعل المؤشر p يؤشر على المحرف التالي في السلسلة أي على $.
    ثم لو قمنا مرةً أخرى بإضافة 1 إلى المؤشر، فإنه سيؤشر على المحرف التالي أي على #.
    ثم لو كررنا ذلك مرة أخرى، أي لو قمنا بإضافة 1، فإنه سيؤشر على المحرف 0\ أي على 0 (نهاية السلسلة).

الآن بالاستفادة من الملاحظات السابقة سنقوم بتعريف الدالة المرنة المطلوبة.

اشمل "مـتم/طـرفية"؛
اشمل "مـتم/نـص"؛
استخدم مـتم؛
دالة اطبع_المعطيات (نسق: مؤشر[مـحرف]، معطيات: ...أيما) {
  // استمر في الطباعة طالما لم تنته سلسلة المحارف
  بينما نسق~محتوى != 0 {
    إذا نسق~محتوى == '#' {
      طـرفية.اطبع("%d\ج"، معطيات~المعطى_التالي[صحيح])؛ // المعطى عدد صحيح
    } وإلا {
      طـرفية.اطبع("%s\ج"، (معطيات~المعطى_التالي[نـص].صوان))؛ // إذاً فهو سلسلة
    }
    نسق = نسق + 1؛ // الإزاحة بمقدار بايت واحد
  }
}
عرف نص: نـص = "مرحبًا"؛
اطبع_المعطيات("#$#"، 5، نص، 5)؛
/*
5
مرحبًا
5
*/
import "Srl/Console.alusus";
import "Srl/String.alusus";
use Srl;
function printArguments (format: ptr[Char], args: ...any) {
  // استمر في الطباعة طالما لم تنته سلسلة المحارف
  while format~cnt != 0 {
    if format~cnt == '#' {
      Console.print("%d\n", args~next_arg[Int]); // المُعطى عدد صحيح
    } else {
      Console.print("%s\n", (args~next_arg[String]).buf); // إذاً فهو سلسلة
    }
    format = format + 1; // الإزاحة بمقدار بايت واحد
  }
}
def str: String = "Hello";
printArguments("#$#", 5, str, 5);
/*
5
Hello
5
*/