البرمجة العامّة (Generics) في Ada ── كتابة العقود بالأنواع، وتحقيق إعادة الاستخدام دون تكلفة إضافيّة

· آخر تحديث: · · Ada, لغة برمجة, الجينيريكس, نظام الأنواع, الأنواع الساكنة, نموذج العقد, التجريد بلا تكلفة, GNAT, Alire, الموثوقية العالية, إعادة استخدام الشيفرة

1. مقدّمة ── ليس «استقبال أيّ شيء»، بل «ما الذي يُوعَد به»

عند محاولة كتابة شيفرة قابلة لإعادة الاستخدام بلغة ذات أنواع ساكنة (statically typed)، سرعان ما تصطدم بالمعضلة نفسها. تريد استخدام الكومة (stack) المكتوبة للأعداد الصحيحة مع النصوص أيضاً. تريد استخدام معالجة إحصائيّة واحدة لمصفوفة أعداد الفاصلة العائمة أيضاً. تريد استخدام منطق الترتيب التصاعديّ للترتيب التنازليّ أيضاً. لكن عند نسخ الشيفرة نفسها لكلّ نوع، تحدث إغفالات في التعديل. وعلى النقيض، عند تصميم يستقبل أيّ شيء عبر void* أو التحويل القسريّ (cast)، تنهار سلامة الأنواع.

إجابة Ada هي الجينيريكس (generic units).

جينيريكس Ada ليست مجرّد استبدال نصّيّ. تستقبل الأنواع، والقيم، والإجراءات، بل والحزم نفسها كمعطيات فرعيّة، ويُفحَص نوعها سكونيّاً عند إنشاء النسخة. بعبارة أخرى، بدلاً من فحص «هل هذا النوع صحيح فعلاً» وقت التشغيل، هي آليّة تُحدِّد وقت الترجمة «هل يفي هذا المكوِّن بهذا العقد».

شيفرة تريد إعادة استخدامهاكيف تُعاد استخدامها؟نسخ ولصقvoid* / كائن (Object) / تحويل قسريّ (cast)جينيريكس Adaيسهل إغفال التعديلاتأخطاء وقت التشغيل وانهيار سلامة الأنواع شائعانآمن نوعيّاًيُفحَص وقت الترجمةبلا استدعاء غير مباشر إضافيّ وقت التشغيل

في هذا المقال، نرتِّب البرمجة العامّة في Ada وفق التسلسل التالي:

  • الإجراءات العامّة (generic subprograms)
  • الحزم العامّة (generic packages)
  • معطيات الأنواع، ومعطيات القيم، ومعطيات الإجراءات الفرعيّة
  • فئات الأنواع مثل private وَrange <> وَdigits <>
  • أمثلة تنفيذ للترتيب، والكومة، والمعالجة الإحصائيّة، وَCount_If، ومخزن مفتاح-قيمة
  • الجينيريكس عالي الرتبة عبر معطى الحزمة الرسميّ
  • نموذج عقد (contract model) في Ada وأفكار التصميم العمليّ

يأتي هذا الموضوع تالياً لسلسلة مدوّنتنا: «سحر لغة Ada» و«مدخل إلى التحقّق الشكليّ عبر SPARK» و«المعالجة المتوازية الآمنة» و«أنظمة الوقت الفعليّ». نتعمّق في فكرة Ada القائمة على «التعبير عن التصميم بالأنواع» من زاوية الجينيريكس.

2. خريطة هذا المقال

لنُمسك بالصورة الكاملة أوّلاً عبر رسم. إذا فُهمت جينيريكس Ada على أنّها «ميّزة تستقبل النوع كمعطى» فقط، فهذا فهم ضيّق جدّاً. في الواقع، وفقاً للوحدة المراد إعادة استخدامها، تُدمَج الإجراءات، والحزم، ومعطيات الإجراءات الفرعيّة، ومعطيات القيم، ومعطى الحزمة الرسميّ.

جينيريكس Adaإجراء عامّSwapCount_IfSortحزمة عامّةStackStatisticsمخزن مفتاح-قيمةالمعطيات الفرعيّةنوعprivatelimited privaterange boxmod boxdigits boxdelta boxdiscrete boxقيمةMax_SizeThresholdإجراء فرعيّLessEqualsPredicateحزمةwith package P is newGenericأفكار التصميمنموذج العقدالفحص الساكنالتجريد بلا تكلفة إضافيّةفصل المواصفة عن الجسم

طريقة قراءة هذا المقال بسيطة. في النصف الأوّل نتتبّع الصياغة، وفي النصف الثاني نتناول قرارات التصميم. لمن يقرأ Ada لأوّل مرّة، لا تحاول في البداية حفظ تفاصيل الصياغة، بل انتبه إلى «ما الذي جُعل معطى فرعيّاً» و«ما العمليّات المسموح بها لذلك المعطى الفرعيّ».

3. بيئة التشغيل وطريقة الترجمة

تفترض شيفرة هذا المقال GNAT 15.x فما بعده. GNAT مترجم Ada التمثيليّ، ويمكن تثبيته عبر Alire. Alire مدير حزم Ada / SPARK، ويمكن استخدامه أيضاً لإدارة سلسلة الأدوات والبناء.

gnat --version
# GNAT 15.2.1

ثبِّت GNAT من Alire (مدير حزم Ada) عبر alr install gnat_native gprbuild، واضبط PATH.

الأمثلة الواردة في هذا المقال يُفترَض وضعها داخل المستودع على النحو التالي.

ada-generic-programming/src/snippets/01_swap.ada02_stack.ada03_sort.ada04_statistics.ada05_filter.ada06_kv_store.adaREADME.md

الأمثلة التي تجمع عدّة وحدات ترجمة في ملفّ واحد، تُقسَّم أوّلاً عبر gnatchop ثمّ تُبنى عبر gnatmake.

mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo

-gnata خيار لتفعيل التأكيدات (assertions). ليس ضروريّاً لاستخدام الجينيريكس نفسها، لكنّه يسهِّل التحقّق من العقود وشروط الحدود في أمثلة التعلّم.

ملفّ تنفيذيّgnatmakegnatchopمطوِّرملفّ تنفيذيّgnatmakegnatchopمطوِّرتمرير ملفّ .ada واحدتقسيم إلى .ads / .adb / رئيسيّ (main)gnatmake -gnata mainتنفيذ الربط (binding) والوصل (linking)./mainنتيجة التنفيذ

4. النموذج الأساسيّ لجينيريكس Ada

يسهل فهم جينيريكس Ada إذا نظرنا إليها تقريباً عبر ثلاثة عناصر:

  1. كتابة الوحدة العامّة (generic unit)
  2. كتابة المعطى الفرعيّ (formal parameter) في جزء generic
  3. إنشاء نسخة (instantiation) عبر new من جانب المستخدم
إعلان الجينيريكالمعطيات الفرعيّةجسم الجينيريكإنشاء نسخة عبر newيُستخدَم كإجراء أو حزمة عاديّةنوعقيمةإجراء فرعيّحزمة

على سبيل المثال، إذا جُعلت عمليّة تبديل قيمتين عامّة، يمكن جعل النوع فقط معطى فرعيّاً.

generic
   type Element is private;
procedure Generic_Swap (A, B : in out Element);

في هذه المرحلة، لا يمكن استدعاء Generic_Swap بعد. هذا «قالب لعمليّة تبديل تُستخدَم لأيّ نوع Element». يصبح إجراءً عاديّاً فقط بعد منحه نوعاً محدَّداً.

procedure Swap_Integer is new Generic_Swap (Integer);

في صورة رسم، العلاقة كالتالي:

تمرير Integerتمرير Characterتمرير My_RecordGeneric_Swaptype Element is privateSwap_IntegerSwap_CharacterSwap_My_Recordيبدِّل متغيّرات Integerيبدِّل متغيّرات Characterيبدِّل متغيّرات My_Record

المهمّ هو أنّ جسم القالب مكتوب باستخدام العمليّات المُتاحة لِـ Element فقط. عند الإعلان type Element is private;، تُتاح العمليّات الأساسيّة كالإسناد والمقارنة بالتساوي، لكن لا تُتاح المقارنة الكبرى/الصغرى أو العمليّات الحسابيّة. بعبارة أخرى، فإنّ إعلان الجينيريك نفسه يُعبِّر عن «ما الذي يجوز لهذا المكوِّن افتراضه».

5. أنواع المعطيات الفرعيّة ── مفردات جينيريكس Ada

ما يمكن أن تستقبله جينيريكس Ada ليس النوع فقط. هذه نقطة اختلاف كبيرة مقارنةً بجينيريكس C# أو Java المعتادة.

المعطيات الفرعيّة للجينيريكمعطى نوعمعطى قيمةمعطى إجراء فرعيّمعطى حزمةtype Element is privatetype Index is boxtype Real is digits boxMax_Size : PositiveDefault_Value : Elementwith function Less...with procedure Put ...with package P is new ...

إذا رتّبنا المعطيات الفرعيّة النموذجيّة في جدول، يكون كالتالي:

النوع مثال المعنى
معطى نوع type Element is private; الصورة الأساسيّة لاستقبال أيّ نوع definite غير limited
معطى نوع limited type Element is limited private; يستقبل أيضاً الأنواع التي لا يمكن نسخها
نوع منفصل (discrete) type Index is (<>); نوع يمكن استخدامه كفهرس مصفوفة، كالأنواع الصحيحة أو التعداديّة
نوع صحيح موقَّع type Count is range <>; يمكن افتراض عمليّات صحيحة مثل +، -، والمقارنة الكبرى/الصغرى
نوع صحيح إجرائيّ (modular) type Word is mod <>; للتعامل مع العمليّات البتّيّة والأعداد الصحيحة الدوريّة
نوع فاصلة عائمة type Real is digits <>; مثل Float، وَLong_Float، والأنواع العائمة المُعرَّفة من المستخدم
نوع فاصلة ثابتة type Money is delta <>; للتعامل مع العمليّات الحسابيّة ذات الفاصلة الثابتة
معطى قيمة Max_Size : Positive; لتثبيت الحجم أو العتبة وغيرهما لكلّ نسخة
إجراء فرعيّ with function Predicate (...) return Boolean; لحقن سلوك مثل دالّة مقارنة أو مُسنَد
حزمة with package P is new Some_Generic (<>); لاستقبال حزمة جينيريك مُنشَأة مسبقاً كمكوِّن

بامتلاك هذه المفردات، يمكن في Ada طبيعيّاً كتابة «استقبال الأنواع القادرة على هذه العمليّة فقط» بدلاً من «استقبال أيّ شيء، مع القيام بأمور خطرة داخليّاً».

6. الإجراء العامّ ── فهم أبسط بنية عبر Generic_Swap

كمثال أوّل، لنُلقِ نظرة على Generic_Swap الذي يبدِّل متغيّرين من أيّ نوع.

6.1 جزء المواصفة

generic
   type Element is private;
procedure Generic_Swap (A, B : in out Element);

الجزء التالي لـgeneric هو المعطى الفرعيّ. هنا نستقبل نوعاً باسم Element. is private تعني أنّه من منظور جسم الجينيريك، لا يُعرَف التمثيل الداخليّ لهذا النوع.

يتّضح من هذا الإعلان أمران:

  • يمكن استخدام Generic_Swap لأيّ نوع Element
  • لا يعتمد الجسم على البنية الداخليّة لِـ Element أو على المقارنة الكبرى/الصغرى

6.2 الجسم

procedure Generic_Swap (A, B : in out Element) is
   Temp : constant Element := A;
begin
   A := B;
   B := Temp;
end Generic_Swap;

في هذا الجسم، لا يُستخدَم مع Element سوى الإسناد. لا يُستخدَم A < B ولا A + B. لذلك، يمكن استخدامه طبيعيّاً مع Integer، وَCharacter، وأنواع السجلّات، والأنواع التعداديّة، وأيّ نوع قابل للإسناد.

بعد الاستدعاءقبل الاستدعاءA = 20B = 10A = 10B = 20Temp = A

6.3 إنشاء النسخة

يستخدم المستخدم new.

procedure Swap_Int  is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);

بهذا يصبح Swap_Int وَSwap_Char قابلين للاستدعاء كإجراءين عاديّين.

with Ada.Text_IO; use Ada.Text_IO;

procedure Swap_Demo is
   generic
      type Element is private;
   procedure Generic_Swap (A, B : in out Element);

   procedure Generic_Swap (A, B : in out Element) is
      Temp : constant Element := A;
   begin
      A := B;
      B := Temp;
   end Generic_Swap;

   procedure Swap_Int is new Generic_Swap (Integer);

   X : Integer := 10;
   Y : Integer := 20;
begin
   Put_Line ("Before: X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
   Swap_Int (X, Y);
   Put_Line ("After : X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
end Swap_Demo;

صورة التنفيذ كالتالي.

Before: X= 10, Y= 20
After : X= 20, Y= 10

هنا لا يمكن تمرير متغيّر من نوع Float إلى Swap_Int (X, Y);. لأنّ Swap_Int إجراء عاديّ أُنشئت نسخته لِـ Integer. يسهل فهم الجينيريكس إذا اعتُبرت «آليّة لإنشاء نسخة آمنة لكلّ نوع» بدلاً من «فتحة تستقبل أيّ شيء».

7. الحزمة العامّة ── جعل النوع والقيمة معطيات

عندما تريد إعادة استخدام مجموعة من العمليّات والحالة الداخليّة معاً، لا إجراءً واحداً فقط، تُستخدَم الحزمة العامّة. المثال النموذجيّ هو الكومة (stack).

في الكومة، طالما تغيّر نوع العنصر وحجمه الأقصى فقط، يبقى المنطق الأساسيّ واحداً.

Generic_StackElement_TypeMax_SizePush / Pop / Size / Is_Empty / Is_FullInt_StackElement=IntegerMax_Size=5Float_StackElement=FloatMax_Size=3String_StackElement=Unbounded_StringMax_Size=20

7.1 جزء المواصفة

generic
   type Element_Type is private;
   Max_Size : Positive;
package Generic_Stack is
   procedure Push (Item : Element_Type);
   function Pop return Element_Type;
   function Is_Empty return Boolean;
   function Is_Full  return Boolean;
   function Size return Natural;

   Stack_Overflow  : exception;
   Stack_Underflow : exception;
end Generic_Stack;

نستخدم هنا نوعين من المعطيات الفرعيّة.

  • Element_Type معطى نوع
  • Max_Size معطى قيمة

بما أنّ Max_Size من نوع Positive، لا يمكن إنشاء نسخة بحجم 0 أو أقلّ. بهذا الشكل، يمكن أيضاً منح معطى القيمة قيداً بواسطة نوعه.

7.2 الجسم

package body Generic_Stack is
   subtype Index_Type is Positive range 1 .. Max_Size;
   type Storage_Type is array (Index_Type) of Element_Type;

   Data : Storage_Type;
   Top  : Natural := 0;

   procedure Push (Item : Element_Type) is
   begin
      if Top = Max_Size then
         raise Stack_Overflow;
      end if;

      Top := Top + 1;
      Data (Top) := Item;
   end Push;

   function Pop return Element_Type is
      Result : Element_Type;
   begin
      if Top = 0 then
         raise Stack_Underflow;
      end if;

      Result := Data (Top);
      Top := Top - 1;
      return Result;
   end Pop;

   function Is_Empty return Boolean is
   begin
      return Top = 0;
   end Is_Empty;

   function Is_Full return Boolean is
   begin
      return Top = Max_Size;
   end Is_Full;

   function Size return Natural is
   begin
      return Top;
   end Size;
end Generic_Stack;

المهمّ في جسم هذه الحزمة هو أنّ Data وَTop يُنشَآن بشكل منفصل لكلّ نسخة.

package Int_Stack   is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float,   3);

هذان أُنشئا من القالب نفسه، لكنّهما لا يتشاركان الحالة الداخليّة.

حالة Float_StackTopData: مصفوفة Floatحالة Int_StackTopData: مصفوفة IntegerGeneric_StackInt_StackFloat_Stack

7.3 انتقالات حالة الكومة

يسهل فهم الكومة إذا نُظر إليها كآلة حالات.

PushPush / PopPop يزيل العنصر الأخيرPush يصل إلى Max_SizePopPushPopفارغةغير فارغةممتلئةفيضاننقصان

7.4 مثال استخدام

with Ada.Text_IO; use Ada.Text_IO;

procedure Stack_Demo is
   generic
      type Element_Type is private;
      Max_Size : Positive;
   package Generic_Stack is
      procedure Push (Item : Element_Type);
      function Pop return Element_Type;
      function Is_Empty return Boolean;
      function Is_Full  return Boolean;
      function Size return Natural;
      Stack_Overflow  : exception;
      Stack_Underflow : exception;
   end Generic_Stack;

   package body Generic_Stack is
      subtype Index_Type is Positive range 1 .. Max_Size;
      type Storage_Type is array (Index_Type) of Element_Type;

      Data : Storage_Type;
      Top  : Natural := 0;

      procedure Push (Item : Element_Type) is
      begin
         if Top = Max_Size then
            raise Stack_Overflow;
         end if;

         Top := Top + 1;
         Data (Top) := Item;
      end Push;

      function Pop return Element_Type is
         Result : Element_Type;
      begin
         if Top = 0 then
            raise Stack_Underflow;
         end if;

         Result := Data (Top);
         Top := Top - 1;
         return Result;
      end Pop;

      function Is_Empty return Boolean is (Top = 0);
      function Is_Full  return Boolean is (Top = Max_Size);
      function Size     return Natural is (Top);
   end Generic_Stack;

   package Int_Stack is new Generic_Stack (Integer, 5);
begin
   Int_Stack.Push (10);
   Int_Stack.Push (20);
   Int_Stack.Push (30);

   Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
   Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
   Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
   Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
end Stack_Demo;

تُظهِر الحزمة العامّة فاعليّتها في العمل الفعليّ في «الحاويات الصغيرة»، و«المخازن المؤقّتة الثابتة الطول»، و«المخازن المؤقّتة الحلقيّة (ring buffer)»، و«طوابير السجلّ (log)»، و«طبقة تجريد العتاد» وغيرها. وخصوصاً في Ada، فإنّ تصميماً يُثبِّت الحجم سكونيّاً كمعطى نوع أو قيمة، بدلاً من تحريكه في وقت التشغيل، يعمل بتناغم مع الأنظمة عالية الموثوقيّة.

8. المعطى الفرعيّ للإجراء ── حقن السلوك

استقبال النوع وحده لا يكفي أحياناً للتعبير عمّا نريده. في الترتيب مثلاً، لا يلزم نوع العنصر فقط، بل يلزم أيضاً منطق المقارنة الذي يحدِّد «أيّهما يُصفّ أوّلاً».

في Ada، يمكن جعل دالّة المقارنة هذه معطى فرعيّاً للجينيريك.

Generic_Insertion_SortItem_TypeIndexItem_Arrayدالّة المقارنةاستخدام المقارنة المعياريّةتمرير Greater للترتيب التنازليّتمرير ترتيب مخصَّص

8.1 جزء المواصفة

generic
   type Item_Type is private;
   type Index is (<>);
   type Item_Array is array (Index range <>) of Item_Type;
   with function "<" (Left, Right : Item_Type) return Boolean is <>;
procedure Generic_Insertion_Sort (Items : in out Item_Array);

هنا أربعة معطيات فرعيّة.

  1. Item_Type: نوع عنصر المصفوفة
  2. Index: نوع فهرس المصفوفة
  3. Item_Array: نوع المصفوفة الفعليّ
  4. "<": دالّة المقارنة

type Index is (<>); تستقبل نوعاً منفصلاً (discrete). ليس فقط الأنواع الصحيحة، بل الأنواع التعداديّة أيضاً. من طابع Ada استخدام نوع تعداديّ مثل Day كفهرس مصفوفة، لا الاكتفاء بـ Positive.

is <> في with function "<" ... is <>; تعني أنّه إذا حُذف المعطى الفعليّ، تُستخدَم عمليّة مؤثِّر (operator) معياريّة مرئيّة أو دالّة متطابقة. بعبارة أخرى، بالنسبة لنوع لديه < بالفعل مثل Integer، يمكن استخدامه دون تحديد دالّة المقارنة صراحةً.

8.2 الجسم

procedure Generic_Insertion_Sort (Items : in out Item_Array) is
   J   : Index;
   Key : Item_Type;
begin
   if Items'Length <= 1 then
      return;
   end if;

   for I in Index'Succ (Items'First) .. Items'Last loop
      Key := Items (I);
      J := I;

      while J > Items'First and then Key < Items (Index'Pred (J)) loop
         Items (J) := Items (Index'Pred (J));
         J := Index'Pred (J);
      end loop;

      Items (J) := Key;
   end loop;
end Generic_Insertion_Sort;

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

نعملالانعممصفوفة غير مرتَّبةأخذ Key من اليسارهل يسبق Key العنصر السابق؟إزاحة العنصر السابق يميناًإدراج Keyهل وصلنا إلى النهاية؟مصفوفة مرتَّبة

8.3 إنشاء التصاعديّ والتنازليّ من الجسم نفسه

type Int_Array is array (Positive range <>) of Integer;

procedure Sort_Asc is new Generic_Insertion_Sort
  (Item_Type  => Integer,
   Index      => Positive,
   Item_Array => Int_Array);

function Greater (Left, Right : Integer) return Boolean is
  (Left > Right);

procedure Sort_Desc is new Generic_Insertion_Sort
  (Item_Type  => Integer,
   Index      => Positive,
   Item_Array => Int_Array,
   "<"        => Greater);

يستخدم Sort_Asc المقارنة < المعياريّة. أمّا Sort_Desc فيستبدل دالّة المقارنة عبر "<" => Greater.

99, 3, 47, 12Sort_Ascالمقارنة المعياريّةSort_Descيمرِّر Greater كمقارنة3, 12, 47, 9999, 47, 12, 3

هذه الآليّة قريبة من تصميم تمرير كائن دالّة المقارنة كمعطى قالب في C++، أو تصميم طلب الترتيب عبر حدود السمات (trait bounds) في Rust. غير أنّ Ada تُصرِّح بوضوح، كمعطى فرعيّ للإجراء، بأنّه «يُمرَّر دالّة بهذا الشكل».

9. فئات الأنواع ── كتابة عقد أكثر تحديداً من private

type T is private; مفيد، لكنّه لا يستطيع كلّ شيء. بالنسبة لنوع private، لا يمكن استخدام العمليّات الحسابيّة الأربع أو المقارنة الكبرى/الصغرى بشكل بديهيّ. لذلك تسمح Ada بتحديد فئة (category) للمعطى الفرعيّ للنوع.

معطى النوعprivatelimited privatediscrete box: نوع منفصل (discrete)range box: نوع صحيح موقَّعmod box: نوع صحيح إجرائيّ (modular)digits box: نوع فاصلة عائمةdelta box: نوع فاصلة ثابتةنوع accessنوع تعداديّنوع صحيحFloatLong_Floatنوع فاصلة عائمة معرَّف من المستخدم

9.1 ما الفائدة من تحديد الفئة

لحساب المتوسّط أو التباين مثلاً، تلزم عمليّات الجمع والطرح والضرب والقسمة. لا يمكن افتراض هذه العمليّات مع النوع private. لذا نقيِّده بنوع الفاصلة العائمة.

generic
   type Real is digits <>;
   type Real_Array is array (Positive range <>) of Real;
package Generic_Statistics is
   function Mean (Values : Real_Array) return Real;
   function Variance (Values : Real_Array) return Real;
end Generic_Statistics;

بـ type Real is digits <>;، يُعلَم أنّ Real نوع فاصلة عائمة. لذا يمكن استخدام +، وَ-، وَ*، وَ/ وغيرها في جسم الجينيريك.

9.2 الجسم

package body Generic_Statistics is
   function Mean (Values : Real_Array) return Real is
      Sum : Real := 0.0;
   begin
      if Values'Length = 0 then
         return 0.0;
      end if;

      for V of Values loop
         Sum := Sum + V;
      end loop;

      return Sum / Real (Values'Length);
   end Mean;

   function Variance (Values : Real_Array) return Real is
      M   : constant Real := Mean (Values);
      Sum : Real := 0.0;
   begin
      if Values'Length = 0 then
         return 0.0;
      end if;

      for V of Values loop
         declare
            D : constant Real := V - M;
         begin
            Sum := Sum + D * D;
         end;
      end loop;

      return Sum / Real (Values'Length);
   end Variance;
end Generic_Statistics;

9.3 الاستخدام مع Float وَLong_Float

type Float_Array is array (Positive range <>) of Float;
type Long_Array  is array (Positive range <>) of Long_Float;

package Float_Stats is new Generic_Statistics (Float, Float_Array);
package Long_Stats  is new Generic_Statistics (Long_Float, Long_Array);

يمكن إعادة استخدام المعالجة الإحصائيّة نفسها لأنواع فاصلة عائمة مختلفة الدقّة.

Generic_StatisticsReal is digits boxFloat_StatsLong_StatsMy_Real_StatsMean / Variance مع FloatMean / Variance مع Long_FloatMean / Variance مع Real معرَّف من المستخدم

9.4 تحديد الفئة «مواصفة على مستوى النوع»

تحديد الفئة ليس مجرّد صياغة لإسكات المترجم. إنّه يصبح أيضاً مواصفة تُخبِر القارئ بما «يتطلّبه هذا المكوِّن».

العمليّة المراد كتابتها المعطى الفرعيّ المناسب السبب
التبديل، والحفظ، والاسترجاع private يكفي أن يكون قابلاً للإسناد
إدارة موارد لا يمكن نسخها limited private لا يُفتَرض الإسناد
فهرس مصفوفة، تصفّح حالة تعداديّة (<>) يمكن استخدام First، وَLast، وَSucc، وَPred
مجموع الأعداد الصحيحة، العدّاد range <> يمكن افتراض العمليّات الحسابيّة الصحيحة
قناع بتّيّ، عدّاد دوريّ mod <> يمكن افتراض العمليّات الإجرائيّة (modular)
المتوسّط، التباين، الحساب الرقميّ digits <> يمكن افتراض عمليّات الفاصلة العائمة
المبلغ، كميّة التحكّم بدقّة ثابتة delta <> يمكن افتراض عمليّات الفاصلة الثابتة

10. حقن المُسنَد ── كتابة Count_If بأسلوب Ada

يمكن استخدام المعطى الفرعيّ للإجراء ليس فقط لدالّة المقارنة، بل للمُسنَد (predicate) أيضاً. المُسنَد دالّة تستقبل قيمة وتُعيد Boolean.

الدور الذي يؤدّيه Func<T, bool> في C#، وَPredicate<T> في Java، ودوال لامدا أو كائنات الدوال في C++، يمكن التعبير عنه في Ada كمعطى فرعيّ لإجراء الجينيريك.

10.1 جزء المواصفة

generic
   type Element is private;
   type Index is (<>);
   type Array_Type is array (Index range <>) of Element;
   with function Predicate (Item : Element) return Boolean;
function Generic_Count_If (Arr : Array_Type) return Natural;

هنا لم نضف is <> إلى Predicate. بما أنّه لا توجد دالّة مُسنَد معياريّة مرئيّة، فهذا تصميم يُلزِم المستخدم بتمريرها دائماً.

10.2 الجسم

function Generic_Count_If (Arr : Array_Type) return Natural is
   Count : Natural := 0;
begin
   for Item of Arr loop
      if Predicate (Item) then
         Count := Count + 1;
      end if;
   end loop;

   return Count;
end Generic_Count_If;

تدفّق المعالجة بسيط.

TrueFalseمصفوفةتصفّح كلّ عنصرPredicate(Item)?زيادة Countلا تفعل شيئاًالعنصر التاليإرجاع Count

10.3 عدّ الأعداد الزوجيّة وعدّ ما فوق العتبة

type Int_Array is array (Positive range <>) of Integer;

function Is_Even (N : Integer) return Boolean is
  (N mod 2 = 0);

function Is_Large (N : Integer) return Boolean is
  (N > 50);

function Count_Even is new Generic_Count_If
  (Element    => Integer,
   Index      => Positive,
   Array_Type => Int_Array,
   Predicate  => Is_Even);

function Count_Large is new Generic_Count_If
  (Element    => Integer,
   Index      => Positive,
   Array_Type => Int_Array,
   Predicate  => Is_Large);

من منطق التصفّح نفسه، يمكن إنشاء دالّتين تختلفان في الشرط فقط.

Generic_Count_IfCount_EvenPredicate = Is_EvenCount_LargePredicate = Is_Large12, 7, 88, 3, 56, 91, 44, 19, 62عدد الأعداد الزوجيّةالعدد الذي يفوق 50

في هذا المثال، تصفّح المصفوفة، وإدارة العدّاد، وإرجاع النتيجة، كلّها مشتركة. أمّا «ما الذي نعدّه» فيُحقَن كدالّة فقط. هذا هو الشكل الأساسيّ للتصميم عالي الرتبة في Ada.

11. تركيب معطيات متعدّدة ── مخزن مفتاح-قيمة عامّ

في المكوِّنات الواقعيّة، نادراً ما يكفي معطى نوع واحد فقط. يلزم دمج عدّة شروط، مثل نوع المفتاح والقيمة، وطريقة مقارنة المفتاح، والعدد الأقصى.

هنا نأخذ مثالاً لمخزن مفتاح-قيمة بسيط ثابت الطول.

Generic_KV_StoreKey_TypeValue_Typeدالّة تطابق المفتاحMax_EntriesPut / Get / Containsمخزن إعداداتذاكرة تخزين مؤقّت صغيرة الحجمقاموس ثابت الحجم للاستخدام في الأنظمة المدمجة

11.1 جزء المواصفة

generic
   type Key_Type is private;
   type Value_Type is private;
   with function "=" (Left, Right : Key_Type) return Boolean is <>;
   Max_Entries : Positive := 50;
package Generic_KV_Store is
   procedure Put (Key : Key_Type; Val : Value_Type);
   function Get (Key : Key_Type) return Value_Type;
   function Contains (Key : Key_Type) return Boolean;

   Key_Not_Found : exception;
   Store_Full    : exception;
end Generic_KV_Store;

في هذه الحزمة أربعة معطيات فرعيّة.

المعطى النوع الدور
Key_Type نوع نوع المفتاح
Value_Type نوع نوع القيمة
"=" إجراء فرعيّ فحص تطابق المفتاح
Max_Entries قيمة العدد الأقصى للمُدخَلات

مُنحت Max_Entries قيمة افتراضيّة عبر := 50. لذا إن لم تُحدَّد صراحةً، يصبح المخزن بسعة 50 مُدخَلاً.

11.2 الجسم

package body Generic_KV_Store is
   subtype Index_Type is Positive range 1 .. Max_Entries;

   type Key_Array   is array (Index_Type) of Key_Type;
   type Value_Array is array (Index_Type) of Value_Type;
   type Used_Array  is array (Index_Type) of Boolean;

   Keys   : Key_Array;
   Values : Value_Array;
   Used   : Used_Array := (others => False);

   function Find_Index (Key : Key_Type) return Natural is
   begin
      for I in Index_Type loop
         if Used (I) and then Keys (I) = Key then
            return I;
         end if;
      end loop;

      return 0;
   end Find_Index;

   function Find_Free return Natural is
   begin
      for I in Index_Type loop
         if not Used (I) then
            return I;
         end if;
      end loop;

      return 0;
   end Find_Free;

   procedure Put (Key : Key_Type; Val : Value_Type) is
      Pos : Natural := Find_Index (Key);
   begin
      if Pos = 0 then
         Pos := Find_Free;

         if Pos = 0 then
            raise Store_Full;
         end if;

         Used (Pos) := True;
         Keys (Pos) := Key;
      end if;

      Values (Pos) := Val;
   end Put;

   function Get (Key : Key_Type) return Value_Type is
      Pos : constant Natural := Find_Index (Key);
   begin
      if Pos = 0 then
         raise Key_Not_Found;
      end if;

      return Values (Pos);
   end Get;

   function Contains (Key : Key_Type) return Boolean is
   begin
      return Find_Index (Key) /= 0;
   end Contains;
end Generic_KV_Store;

هذا التنفيذ بحث خطّيّ (linear search)، فهو لا يناسب البيانات الكبيرة. لكن في المواضع التي تكون فيها خاصّيّة «ثابت الطول، صغير الحجم، بلا حجز ذاكرة ديناميكيّ» مهمّة، هذا شكل سهل الاستخدام.

Keys/Values/Usedنسخة Generic_KV_StoreالمُستدعيKeys/Values/Usedنسخة Generic_KV_Storeالمُستدعيalt[مفتاح موجود][مفتاح جديد]Put(Key, Value)Find_Index(Key)Values(Pos) := ValueFind_FreeKeys(Pos) := KeyValues(Pos) := ValueUsed(Pos) := TrueGet(Key)Find_Index(Key)PosValues(Pos)

11.3 مثال إنشاء نسخة

with Ada.Strings.Unbounded;
use Ada.Strings.Unbounded;

procedure KV_Demo is
   package Int_String_Store is new Generic_KV_Store
     (Key_Type    => Integer,
      Value_Type  => Unbounded_String,
      Max_Entries => 10);
begin
   Int_String_Store.Put (1, To_Unbounded_String ("Ada"));
   Int_String_Store.Put (2, To_Unbounded_String ("SPARK"));

   if Int_String_Store.Contains (1) then
      -- يمكن الحصول على القيمة بـ Get (1)
      null;
   end if;
end KV_Demo;

حُذفت "=". لأنّ Integer لديه عمليّة تساوٍ معياريّة، ويُستخدَم عبر is <>.

إن كان المفتاح مثلاً نصّاً لا يفرِّق بين الحروف الكبيرة والصغيرة، يمكن تمرير دالّة تساوٍ مخصَّصة.

function Same_Key (Left, Right : Unbounded_String) return Boolean is
  (To_Lower (To_String (Left)) = To_Lower (To_String (Right)));

package String_Key_Store is new Generic_KV_Store
  (Key_Type    => Unbounded_String,
   Value_Type  => Integer,
   "="         => Same_Key,
   Max_Entries => 100);

12. معطى الحزمة الرسميّ ── تفكيك الجينيريك إلى مكوِّنات أصغر

يمكن في جينيريكس Ada جعل الحزمة نفسها معطى فرعيّاً. باستخدام هذا، يمكن التعامل مع «نسخة أُنشئت من حزمة جينيريك معيّنة» كمدخل لجينيريك آخر.

Generic_StackInt_StackGeneric_Stack_LoggerInt_Stack_Logger_Instance

12.1 مُسجِّل (logger) يستقبل كومة

لنفترض مثلاً إنشاء مُسجِّل يستقبل نسخة من Generic_Stack السابقة، ويعرض حجمها.

generic
   with package Stack is new Generic_Stack (<>);
package Generic_Stack_Logger is
   procedure Print_Size;
end Generic_Stack_Logger;

يكون الجسم كالتالي.

with Ada.Text_IO; use Ada.Text_IO;

package body Generic_Stack_Logger is
   procedure Print_Size is
   begin
      Put_Line ("Stack size =" & Natural'Image (Stack.Size));
   end Print_Size;
end Generic_Stack_Logger;

يُنشئ المستخدم أوّلاً الكومة، ثمّ يمرِّرها إلى المُسجِّل.

package Int_Stack is new Generic_Stack
  (Element_Type => Integer,
   Max_Size     => 10);

package Int_Stack_Logger is new Generic_Stack_Logger
  (Stack => Int_Stack);

بهذا التصميم، يمكن دمج مكوِّنات الجينيريك مع بعضها.

المرحلة 2المرحلة 1Int_Stack_LoggerGeneric_Stack_LoggerInt_StackGeneric_StackPrint_Size

هذا الاستخدام قريب من معطيات قالب القالب (template template parameters) في C++، لكنّ Ada تُصرِّح بوضوح بـ«استقبال نسخة من هذه الحزمة العامّة». في شيفرة Ada الكبيرة، هذا مفيد عند فصل الحاويات، والخوارزميّات، والسجلّ، والفحص، وأدوات الاختبار المساعدة، ودمجها معاً.

13. نموذج العقد (Contract Model) ── أهمّ فكرة في جينيريكس Ada

من المهمّ لفهم جينيريكس Ada استيعاب نموذج العقد (contract model).

يجب كتابة جسم الجينيريك باستخدام العمليّات التي وعد بها المعطى الفرعيّ فقط. على سبيل المثال، إذا أُعلن فقط type Element is private;، لا يمكن استخدام < مع Element. إن أردنا استخدام <، يجب التصريح به صراحةً كإجراء فرعيّ، أو جعل فئة النوع أكثر تحديداً.

الجزء الفرعيّ للجينيريكالعقدجسم الجينيريكيُنفَّذ ضمن العقديُفحَص نوع الجسم وحدهإنشاء النسخةالمعطيات الفعليّةنوع/دالّة/قيمة فعليّةيتحقّق من وفاء المعطيات الفعليّة بالعقدحزمة أو إجراء عاديّ

بهذا التصميم، يُحمى ليس فقط مستخدم الجينيريك، بل من يكتب الجينيريك أيضاً.

13.1 الفارق في المظهر مع قوالب C++

قوالب C++ قويّة، لكن تاريخيّاً كانت لها خاصّيّة «لا يظهر الخطأ إلا بعد إنشاء نسخة جسم القالب». تحسَّن هذا مع concepts في C++20، لكنّ جينيريكس Ada نموذج يُصرِّح بالعقد صراحةً منذ البداية.

قوالب C++تظهر التعبيرات المطلوبة عند إنشاء النسخةكتابة جسم القالبيمكن لـconcepts توضيح القيود صراحةًAdaيُفحَص الجسم ضمن العقدكتابة العقد في الجزء الفرعيّيتحقّق إنشاء النسخة من المعطيات الفعليّة

جينيريكس Java وC# يتمحور تصميمها حول الأنواع المرجعيّة، والقيود، ومحو الأنواع (type erasure)، وعلاقتها بالتمثيل في وقت التشغيل. أمّا جينيريكس Ada فتميل نحو فكرة إنشاء نسخة محدَّدة في وقت الترجمة.

المنظور Ada C++ Java Rust
طريقة كتابة العقد كتابة النوع والقيمة والدالّة والحزمة في الجزء الفرعيّ templates / concepts معطى نوع وحدود (bounds) حدود السمات (trait bounds)
فحص الجسم يُفحَص ضمن عقد المعطى الفرعيّ يتمحور حول التجسيد وقت الإنشاء يُفحَص ضمن الحدود يُفحَص ضمن حدود السمات
التكلفة في وقت التشغيل الحلّ الساكن هو الأساس التوليد الساكن هو الأساس يتأثّر بمحو الأنواع أحاديّة الشكل هي الأساس
معطى القيمة موجود موجود محدود جينيريكس ثابتة (const generics)
جعل الإجراء الفرعيّ معطى فرعيّاً موجود يُعبَّر عنه بكائنات الدوال وغيرها لامدا/واجهات دالّيّة إغلاقات (closures)/دوال/سمات (traits)
جعل الحزمة معطى فرعيّاً موجود معطيات قالب القالب وغيرها غير موجود يختلف عن بنية الوحدات (modules)

تختلف تفاصيل ميّزات كلّ لغة، لكنّ سمة Ada هي «كتابة العقد كصياغة أوّلاً».

14. قرارات التصميم في العمل الفعليّ ── ما الذي يجب أن يصبح جينيريكاً

الجينيريكس مفيدة، لكن هذا لا يعني أنّ كلّ شيء يجب أن يصبح جينيريكاً. في العمل الفعليّ، يقلّ الفشل إذا اتُّبع الحُكم التالي:

نعملانعملانعملانعملاتوجد منطقيّة تريد إعادة استخدامهاهل يختلف النوع فقط؟فكِّر في معطى نوعهل يختلف الحجم أو العتبة أيضاً؟أضف معطى قيمةهل يختلف سلوك المقارنة أو المُسنَد؟أضف إجراءً فرعيّاًهل تريد حمل حالة داخليّة أو واجهة برمجيّة معاً؟حزمة عامّةيكفي إجراء عاديّ

14.1 المواضع المناسبة للإجراء العامّ

يناسب الإجراء العامّ الخوارزميّات التي لا تحمل حالة.

  • Swap
  • Sort
  • Count_If
  • Find
  • تحويل من نوع Map
  • Min / Max

عندما يكون جسم الخوارزميّة قصيراً والمُدخَل والمُخرَج واضحين، يكون الإجراء أسهل قراءةً من الحزمة.

14.2 المواضع المناسبة للحزمة العامّة

تناسب الحزمة العامّة المواضع التي نريد فيها حمل عدّة عمليّات وحالة داخليّة مع النوع معاً.

  • كومة ثابتة الطول
  • مخزن مؤقّت حلقيّ (ring buffer)
  • قاموس صغير الحجم
  • مجموعة معالجة إحصائيّة
  • تجريد إدخال/إخراج لكلّ جهاز
  • مجموعة عمليّات لنوع رقميّ مزوَّد بوحدة قياس

في Ada خصوصاً، بما أنّ جزء مواصفة الحزمة هو الواجهة العامّة، وجسم الحزمة هو التنفيذ، يمكن استخدام الحزمة العامّة كـ«قالب وحدة آمن نوعيّاً».

مخفيّمواصفة الحزمةالواجهة البرمجيّة العامّةالمُستدعيجسم الحزمةالتنفيذ الداخليّالجزء الفرعيّ للجينيريكعقد النوع/القيمة/الدالّة

14.3 ابدأ بمعطيات فرعيّة قليلة

عندما يزداد عدد المعطيات الفرعيّة، تصبح عمليّة إنشاء النسخة صعبة القراءة. الأسلم أن تبدأ بحدٍّ أدنى، وتزيد عندما يظهر سبب يستدعي الاستبدال.

-- مثال يميل إلى صعوبة القراءة
package X is new Generic_Foo
  (A, B, C, D, E, F, G);

-- الارتباط المُسمَّى يُبقي النيّة واضحة
package X is new Generic_Foo
  (Element_Type => Integer,
   Index_Type   => Positive,
   Buffer_Size  => 128,
   "<"          => Less);

يمكن في Ada استخدام الارتباط المُسمَّى عند إنشاء النسخة. بما أنّ نقاط التصميم المهمّة في الجينيريك تظهر عند إنشاء النسخة، فإنّ الكتابة بأسماء صريحة غالباً ما تكون أسهل صيانة في شيفرة العمل الفعليّ.

15. مواضع التعثّر الشائعة

جينيريكس Ada قويّة، لكن توجد نقاط يسهل التعثّر بها أوّلاً.

15.1 لا يمكن مقارنة الكبر/الصغر مع النوع private

لا يمكن كتابة جسم كالتالي.

generic
   type Element is private;
function Bad_Min (A, B : Element) return Element;

function Bad_Min (A, B : Element) return Element is
begin
   if A < B then      -- خطأ هنا
      return A;
   else
      return B;
   end if;
end Bad_Min;

بما أنّ Element مُعلَن فقط كـprivate، لا يُضمَن أنّ < مُتاحة. إن أردنا المقارنة، نضيفها إلى العقد كالتالي.

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
الجسم يحتاج إلى مقارنة القيمأضف دالّة مقارنة إلى الجزء الفرعيّيُتحقَّق من إمكانيّة المقارنة عند إنشاء النسخةprivate فقطخطأ ترجمة في جسم الجينيريك

15.2 is <> ليست «استدلالاً تلقائيّاً لكلّ شيء»

is <> مفيدة، لكنّها ليست سحراً. عند نقطة إنشاء النسخة، يجب أن تكون العمليّات أو الإجراءات الفرعيّة المطابقة مرئيّة. إذا وُضعت دالّة مقارنة مخصَّصة في حزمة منفصلة، فالأسلم هو استخدام with أو use بشكل مناسب، أو تمريرها صراحةً بالاسم.

procedure Sort_By_Age is new Generic_Insertion_Sort
  (Item_Type  => Person,
   Index      => Positive,
   Item_Array => Person_Array,
   "<"        => Younger_Than);

15.3 يختلف الاستثناء أيضاً لكلّ نسخة

عند إعلان استثناء في جزء المواصفة لحزمة عامّة، يصبح استثناءً مختلفاً لكلّ نسخة.

package Int_Stack   is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);

في هذه الحالة، يُعامَل Int_Stack.Stack_Overflow وَFloat_Stack.Stack_Overflow كاستثناءين مختلفين. إن أردنا التعامل معهما كاستثناء مشترك، فمن الجدير النظر في تصميم يُعرِّف الاستثناء خارج الجينيريك.

استثناء مختلفGeneric_Stackيُعلِن Stack_OverflowInt_Stack.Stack_OverflowFloat_Stack.Stack_Overflow

15.4 قد يزداد حجم الشيفرة

بينما تسهِّل الجينيريكس تجنّب الاستدعاء غير المباشر الزائد في وقت التشغيل، فإنّها بما أنّها تُنشئ نسخة لكلّ نوع، قد يزداد حجم الشيفرة إذا كثر عدد النسخ.

هذه مفاضلة (trade-off) تظهر أيضاً في قوالب C++ وأحاديّة الشكل (monomorphization) في Rust. في التطوير الموجَّه نحو الموثوقيّة العالية والأنظمة المدمجة وأنظمة الوقت الفعليّ، يصبح المنهج هو إدارة حجم مُخرجات البناء مقابل تقليل عدم اليقين في وقت التشغيل.

جسم جينيريك واحدنسخة Integerنسخة Floatنسخة Long_Floatنسخة My_Typeالشيفرة المُولَّدةيسهل تجنّب فحص الأنواع والتغليف (boxing) وقت التشغيلانتبه لتضخّم الحجم مع زيادة عدد النسخ

15.5 المواضع التي يجب فيها استخدام limited private

type Element is private; تفترض قابليّة الإسناد. عند التعامل مع أمور لا نريد نسخها، مثل مقابض الملفّات (file handles)، والأقفال (locks)، ومقابض الأجهزة، من الجدير النظر في limited private.

generic
   type Resource is limited private;
   with procedure Close (R : in out Resource);
procedure Generic_Use_And_Close (R : in out Resource);

في التصميم الذي يتعامل مع أنواع لا يمكن نسخها، يكون التصميم الذي يطبِّق إجراءً أو يُصرِّح بمرجع، أكثر أماناً من الحاوية التي تحفظ القيمة.

16. مجموعة أنماط تصميم صغيرة

من هنا، نرتِّب باختصار الأشكال شائعة الاستخدام في العمل الفعليّ.

16.1 توفير Min للقيم القابلة للمقارنة فقط

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;

function Generic_Min (A, B : Element) return Element is
begin
   if A < B then
      return A;
   else
      return B;
   end if;
end Generic_Min;
Elementيحتاج إلى دالّة مقارنةGeneric_Minيُرجِع القيمة الأصغر

16.2 جعل العتبة معطى قيمة

generic
   type Count_Type is range <>;
   Threshold : Count_Type;
function Generic_Is_Over (Value : Count_Type) return Boolean;

function Generic_Is_Over (Value : Count_Type) return Boolean is
begin
   return Value > Threshold;
end Generic_Is_Over;

يناسب معطى القيمة القيم التي نريد تثبيتها كخاصّيّة للنسخة، لا كإعداد يُحدَّد وقت التشغيل.

16.3 حقن وسيلة الإخراج

generic
   type Element is private;
   with procedure Put (Item : Element);
procedure Generic_Print_Twice (Item : Element);

procedure Generic_Print_Twice (Item : Element) is
begin
   Put (Item);
   Put (Item);
end Generic_Print_Twice;

بهذا الشكل، يمكن استبدال وجهة الإخراج بين الطرفيّة، والسجلّ، والمخزن المؤقّت الخاصّ بالاختبار وغيرها.

Generic_Print_Twiceيستقبل Put كإجراء فرعيّإخراج الطرفيّةإخراج السجلّمخزن مؤقّت للاختبار

16.4 عدم تثبيت نوع فهرس المصفوفة

في Ada، نوع فهرس المصفوفة معلومة نوعيّة مهمّة أيضاً. بدلاً من تثبيته على Positive، يزيد جعل نوع الفهرس معطى فرعيّاً حسب الحاجة من قابليّة إعادة الاستخدام.

generic
   type Element is private;
   type Index is (<>);
   type Array_Type is array (Index range <>) of Element;
procedure Generic_Clear (Arr : in out Array_Type; Value : Element);

بهذا التصميم، يمكن التعامل ليس فقط مع المصفوفات ذات فهرس Positive، بل مع المصفوفات ذات فهرس تعداديّ أيضاً.

Index نوع منفصل (discrete)مجال Positiveتعداد Dayتعداد Stateنوع صحيح مخصَّص

17. قائمة تحقّق لجعل الواجهة البرمجيّة (API) أقرب لأسلوب Ada

عند كتابة جينيريك، تصبح أسهل قراءةً إذا أُعيد النظر فيها من الزوايا التالية في النهاية.

قائمة تحقّق لتصميم الجينيريكهل المعطيات الفرعيّة في حدّها الأدنى؟هل العمليّات المطلوبة صريحة في الجزء الفرعيّ؟هل الفئات مثل private / range / digits مناسبة؟هل يمكن كتابة إنشاء النسخة بوضوح باستخدام الارتباط المُسمَّى؟هل رُوعِيت الحالة والاستثناءات الخاصّة بكلّ نسخة؟هل تضخّم حجم الشيفرة مقبول؟هل تتوفّر نسخة اختباريّة؟

نلخِّصها نصّيّاً كالتالي:

  • اجعل العمليّات المُستخدَمة في الجسم مرئيّة دائماً كعقد للمعطى الفرعيّ.
  • إن كفى private فاستخدمه، وإن لزم الحساب فاستخدم range <> أو digits <>.
  • اجعل السلوك الذي يختلف باختلاف النوع، كالمقارنة والتجزئة (hash) والإخراج والتحويل، إجراءً فرعيّاً.
  • إن كان الحجم أو العتبة خاصّيّة للنسخة، فاجعله معطى قيمة.
  • إن حمل حالة فابدأ بالحزمة العامّة، وإن لم يحمل حالة فابدأ بالإجراء العامّ.
  • في إنشاء النسخة، كلّما زاد عدد المعطيات، استخدم الارتباط المُسمَّى.
  • صمِّم على افتراض أنّ الاستثناءات والحالة الداخليّة تستقلّ لكلّ نسخة.

18. مثال بنية كاملة للأمثلة

إذا فُصلت الأمثلة الواردة في المقال إلى ملفّات، فإنّ البنية التالية تجعل القراءة أسهل.

ada-generic-programmingsrcgenericsdemosgeneric_swap.adsgeneric_swap.adbgeneric_stack.adsgeneric_stack.adbgeneric_insertion_sort.adsgeneric_insertion_sort.adbgeneric_statistics.adsgeneric_statistics.adbgeneric_count_if.adsgeneric_count_if.adbgeneric_kv_store.adsgeneric_kv_store.adbswap_demo.adbstack_demo.adbsort_demo.adbstatistics_demo.adbcount_if_demo.adbkv_demo.adb

بالنسبة للأمثلة الصغيرة الخاصّة بالمقالات، من المريح تجميعها في ملفّ واحد وتقسيمها عبر gnatchop، لكن عند التفكير في العمل الفعليّ أو الصيانة طويلة الأمد، فإنّ الفصل بين المواصفة .ads والجسم .adb بنية أقرب لأسلوب Ada.

19. الخلاصة ── تحديد حدود إعادة الاستخدام بالأنواع

البرمجة العامّة في Ada ليست مجرّد «ميّزة لكتابة شيفرة لا تعتمد على النوع». بل جوهرها التصريح بما يتطلّبه المكوِّن القابل لإعادة الاستخدام، كعقد بالأنواع والإجراءات الفرعيّة والقيم.

اكتب العقداكتب جسم الجينيريكأنشئ نسخة بتمرير نوع/قيمة/دالّةاستخدمه بأمان نوعيّأعد استخدامه دون نسخ

كما رأينا في هذا المقال، يمكن في جينيريكس Ada جعل التالي معطيات فرعيّة:

  • النوع
  • القيمة
  • الإجراء الفرعيّ
  • الحزمة

وبخصوص الأنواع، يمكن تحديد فئات دقيقة جدّاً مثل private، وَlimited private، وَrange <>، وَmod <>، وَdigits <>، وَdelta <>، وَ(<>). بهذا، لا يعتمد جسم الجينيريك على «عمليّة غير معلوم إن كانت ممكنة»، بل يمكنه التنفيذ بأمان باستخدام العمليّات المكتوبة في العقد فقط.

في أصول C أو C++ القديمة، تُستخدَم أحياناً وحدات الماكرو (macros)، وَvoid*، ومؤشِّرات الدوال، وتفريعات الأنواع المكتوبة يدويّاً من أجل إعادة الاستخدام. يمكن لجينيريكس Ada استبدال كثير من هذه الاستخدامات بصيغة آمنة نوعيّاً وسهلة القراءة. خصوصاً في الصيانة طويلة الأمد، والأنظمة المدمجة، وأنظمة الوقت الفعليّ، والبرمجيّات عالية الموثوقيّة، فإنّ هذا «التصميم الذي يحدِّد الحدود وقت الترجمة» يحمل قيمة كبيرة.

20. مجالات الاستشارة ذات الصلة

تتعامل شركة كومورا سوفت المحدودة مع تطوير تطبيقات Windows، وفحص الأصول القائمة وتحديثها، وتنظيم الحدود بين COM / ActiveX / 32bit / 64bit، والاستشارة التقنيّة ومراجعة التصميم. ليس تصميم Ada الساكن الأنواع والموجَّه نحو الموثوقيّة العالية وحده، بل أيضاً كيفيّة تنظيم أصول C/C++ وC# وVB6 وMFC وCOM القائمة من أجل الإطالة أو الترحيل، موضوع قريب في العمل الفعليّ أيضاً.

روابط مرجعيّة

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

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

ما هي جينيريكس Ada؟
آليّة إعادة استخدام تستقبل الأنواع، والقيم، والإجراءات، بل والحزم نفسها كمعطيات فرعيّة (formal parameters)، ويُفحَص نوعها سكونيّاً عند إنشاء نسخة (instantiation) بواسطة new. ليس مجرّد استبدال نصّيّ، بل آليّة تُحدِّد وقت الترجمة «هل يفي هذا المكوِّن بهذا العقد». لا يمكن استدعاء الإجراء العامّ بمجرّد الإعلان عنه، بل يصبح إجراءً أو دالّة عاديّة قابلة للاستخدام فقط بعد إنشاء نسخة منه بنوع محدَّد.
ما الفرق بين جينيريكس Ada وقوالب (templates) C++؟
تعتمد Ada منذ البداية نموذج عقد (contract model) صريحاً، حيث يُكتَب جسم الجينيريك باستخدام العمليّات التي وعد بها المعطى الفرعيّ فقط، ويُفحَص نوع الجسم وحده. أمّا قوالب C++ فتاريخيّاً كانت لا تُظهِر الخطأ إلا بعد إنشاء النسخة، وتحسَّن هذا مع concepts في C++20. كذلك يمكن في Ada، إضافةً إلى معطيات القيم والإجراءات، جعل الحزمة نفسها معطى فرعيّاً، ما يسمح بدمج مكوِّنات جينيريك مع بعضها.
لماذا تُحدَّد فئة النوع (type category) في المعطى الفرعيّ لنوع Ada؟
لتوضيح العمليّات المُستخدَمة في الجسم كعقد صريح. في type T is private، لا يمكن افتراض سوى عمليّات أساسيّة كالإسناد والمقارنة بالتساوي، ولا يمكن استخدام المقارنة الكبرى/الصغرى أو العمليّات الحسابيّة. إذا لزمت العمليّات الحسابيّة الصحيحة تُحدَّد range <>، وإن لزمت عمليّات الفاصلة العائمة تُحدَّد digits <>، وإن لزمت العمليّات البتّيّة تُحدَّد mod <> وما شابه من فئات. كما يعمل تحديد الفئة كمواصفة على مستوى النوع، يُعلِم القارئ بما «يتطلّبه هذا المكوِّن».
ما الذي ينبغي الانتباه إليه في جينيريكس Ada؟
بما أنّه تُنشَأ نسخة لكلّ نوع، فقد يزداد حجم الشيفرة إذا كثر عدد النسخ. هذا مفاضلة (trade-off) تظهر أيضاً في قوالب C++ وأحاديّة الشكل (monomorphization) في Rust. كذلك، فإنّ الاستثناء المُعلَن في الجزء المُتاح (specification) لحزمة عامّة يصبح استثناءً مختلفاً لكلّ نسخة. الإرشاد العمليّ هو البدء بمعطيات فرعيّة قليلة، وزيادتها عندما يظهر سبب يستدعي الاستبدال، واستخدام الارتباط المُسمَّى عند وجود عدد كبير من المعطيات في الإنشاء.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

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

روابط عامة

العودة إلى المدونة