البرمجة العامّة (Generics) في Ada ── كتابة العقود بالأنواع، وتحقيق إعادة الاستخدام دون تكلفة إضافيّة
· آخر تحديث: · غو كومورا · Ada, لغة برمجة, الجينيريكس, نظام الأنواع, الأنواع الساكنة, نموذج العقد, التجريد بلا تكلفة, GNAT, Alire, الموثوقية العالية, إعادة استخدام الشيفرة
1. مقدّمة ── ليس «استقبال أيّ شيء»، بل «ما الذي يُوعَد به»
عند محاولة كتابة شيفرة قابلة لإعادة الاستخدام بلغة ذات أنواع ساكنة (statically typed)، سرعان ما تصطدم بالمعضلة نفسها. تريد استخدام الكومة (stack) المكتوبة للأعداد الصحيحة مع النصوص أيضاً. تريد استخدام معالجة إحصائيّة واحدة لمصفوفة أعداد الفاصلة العائمة أيضاً. تريد استخدام منطق الترتيب التصاعديّ للترتيب التنازليّ أيضاً. لكن عند نسخ الشيفرة نفسها لكلّ نوع، تحدث إغفالات في التعديل. وعلى النقيض، عند تصميم يستقبل أيّ شيء عبر void* أو التحويل القسريّ (cast)، تنهار سلامة الأنواع.
إجابة Ada هي الجينيريكس (generic units).
جينيريكس Ada ليست مجرّد استبدال نصّيّ. تستقبل الأنواع، والقيم، والإجراءات، بل والحزم نفسها كمعطيات فرعيّة، ويُفحَص نوعها سكونيّاً عند إنشاء النسخة. بعبارة أخرى، بدلاً من فحص «هل هذا النوع صحيح فعلاً» وقت التشغيل، هي آليّة تُحدِّد وقت الترجمة «هل يفي هذا المكوِّن بهذا العقد».
flowchart LR
A[شيفرة تريد إعادة استخدامها] --> B{كيف تُعاد استخدامها؟}
B --> C[نسخ ولصق]
B --> D["void* / كائن (Object) / تحويل قسريّ (cast)"]
B --> E[جينيريكس Ada]
C --> C1[يسهل إغفال التعديلات]
D --> D1[أخطاء وقت التشغيل وانهيار سلامة الأنواع شائعان]
E --> E1[آمن نوعيّاً]
E --> E2[يُفحَص وقت الترجمة]
E --> E3[بلا استدعاء غير مباشر إضافيّ وقت التشغيل]
في هذا المقال، نرتِّب البرمجة العامّة في Ada وفق التسلسل التالي:
- الإجراءات العامّة (generic subprograms)
- الحزم العامّة (generic packages)
- معطيات الأنواع، ومعطيات القيم، ومعطيات الإجراءات الفرعيّة
- فئات الأنواع مثل
privateوَrange <>وَdigits <> - أمثلة تنفيذ للترتيب، والكومة، والمعالجة الإحصائيّة، وَ
Count_If، ومخزن مفتاح-قيمة - الجينيريكس عالي الرتبة عبر معطى الحزمة الرسميّ
- نموذج عقد (contract model) في Ada وأفكار التصميم العمليّ
يأتي هذا الموضوع تالياً لسلسلة مدوّنتنا: «سحر لغة Ada» و«مدخل إلى التحقّق الشكليّ عبر SPARK» و«المعالجة المتوازية الآمنة» و«أنظمة الوقت الفعليّ». نتعمّق في فكرة Ada القائمة على «التعبير عن التصميم بالأنواع» من زاوية الجينيريكس.
2. خريطة هذا المقال
لنُمسك بالصورة الكاملة أوّلاً عبر رسم. إذا فُهمت جينيريكس Ada على أنّها «ميّزة تستقبل النوع كمعطى» فقط، فهذا فهم ضيّق جدّاً. في الواقع، وفقاً للوحدة المراد إعادة استخدامها، تُدمَج الإجراءات، والحزم، ومعطيات الإجراءات الفرعيّة، ومعطيات القيم، ومعطى الحزمة الرسميّ.
mindmap
root((جينيريكس Ada))
إجراء عامّ
Swap
Count_If
Sort
حزمة عامّة
Stack
Statistics
مخزن مفتاح-قيمة
المعطيات الفرعيّة
نوع
private
limited private
range box
mod box
digits box
delta box
discrete box
قيمة
Max_Size
Threshold
إجراء فرعيّ
دالّة المقارنة (Less)
دالّة المساواة (Equals)
المُسنَد (Predicate)
حزمة
with package P is new Generic
أفكار التصميم
نموذج العقد
الفحص الساكن
التجريد بلا تكلفة إضافيّة
فصل المواصفة عن الجسم
طريقة قراءة هذا المقال بسيطة. في النصف الأوّل نتتبّع الصياغة، وفي النصف الثاني نتناول قرارات التصميم. لمن يقرأ 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.
الأمثلة الواردة في هذا المقال يُفترَض وضعها داخل المستودع على النحو التالي.
flowchart TB
R[ada-generic-programming/] --> S[src/]
S --> N[snippets/]
N --> A[01_swap.ada]
N --> B[02_stack.ada]
N --> C[03_sort.ada]
N --> D[04_statistics.ada]
N --> E[05_filter.ada]
N --> F[06_kv_store.ada]
R --> README[README.md]
الأمثلة التي تجمع عدّة وحدات ترجمة في ملفّ واحد، تُقسَّم أوّلاً عبر gnatchop ثمّ تُبنى عبر gnatmake.
mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo
-gnata خيار لتفعيل التأكيدات (assertions). ليس ضروريّاً لاستخدام الجينيريكس نفسها، لكنّه يسهِّل التحقّق من العقود وشروط الحدود في أمثلة التعلّم.
sequenceDiagram
participant Dev as مطوِّر
participant Chop as gnatchop
participant Build as gnatmake
participant Exe as ملفّ تنفيذيّ
Dev->>Chop: تمرير ملفّ .ada واحد
Chop-->>Dev: تقسيم إلى .ads / .adb / رئيسيّ (main)
Dev->>Build: gnatmake -gnata main
Build-->>Dev: تنفيذ الربط (binding) والوصل (linking)
Dev->>Exe: ./main
Exe-->>Dev: نتيجة التنفيذ
4. النموذج الأساسيّ لجينيريكس Ada
يسهل فهم جينيريكس Ada إذا نظرنا إليها تقريباً عبر ثلاثة عناصر:
- كتابة الوحدة العامّة (generic unit)
- كتابة المعطى الفرعيّ (formal parameter) في جزء
generic - إنشاء نسخة (instantiation) عبر
newمن جانب المستخدم
flowchart LR
G[إعلان الجينيريك] --> F[المعطيات الفرعيّة]
F --> B[جسم الجينيريك]
B --> I[إنشاء نسخة عبر new]
I --> U[يُستخدَم كإجراء أو حزمة عاديّة]
F --> F1[نوع]
F --> F2[قيمة]
F --> F3[إجراء فرعيّ]
F --> F4[حزمة]
على سبيل المثال، إذا جُعلت عمليّة تبديل قيمتين عامّة، يمكن جعل النوع فقط معطى فرعيّاً.
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
في هذه المرحلة، لا يمكن استدعاء Generic_Swap بعد. هذا «قالب لعمليّة تبديل تُستخدَم لأيّ نوع Element». يصبح إجراءً عاديّاً فقط بعد منحه نوعاً محدَّداً.
procedure Swap_Integer is new Generic_Swap (Integer);
في صورة رسم، العلاقة كالتالي:
flowchart TB
Template[Generic_Swap<br/>type Element is private] -->|تمرير Integer| SwapInt[Swap_Integer]
Template -->|تمرير Character| SwapChar[Swap_Character]
Template -->|تمرير My_Record| SwapRecord[Swap_My_Record]
SwapInt --> ICall[يبدِّل متغيّرات Integer]
SwapChar --> CCall[يبدِّل متغيّرات Character]
SwapRecord --> RCall[يبدِّل متغيّرات My_Record]
المهمّ هو أنّ جسم القالب مكتوب باستخدام العمليّات المُتاحة لِـ Element فقط. عند الإعلان type Element is private;، تُتاح العمليّات الأساسيّة كالإسناد والمقارنة بالتساوي، لكن لا تُتاح المقارنة الكبرى/الصغرى أو العمليّات الحسابيّة. بعبارة أخرى، فإنّ إعلان الجينيريك نفسه يُعبِّر عن «ما الذي يجوز لهذا المكوِّن افتراضه».
5. أنواع المعطيات الفرعيّة ── مفردات جينيريكس Ada
ما يمكن أن تستقبله جينيريكس Ada ليس النوع فقط. هذه نقطة اختلاف كبيرة مقارنةً بجينيريكس C# أو Java المعتادة.
flowchart TB
P[المعطيات الفرعيّة للجينيريك] --> T[معطى نوع]
P --> O[معطى قيمة]
P --> S[معطى إجراء فرعيّ]
P --> PKG[معطى حزمة]
T --> T1[type Element is private]
T --> T2[type Index is box]
T --> T3[type Real is digits box]
O --> O1[Max_Size : Positive]
O --> O2[Default_Value : Element]
S --> S1[with function Less...]
S --> S2[with procedure Put ...]
PKG --> P1[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، وأنواع السجلّات، والأنواع التعداديّة، وأيّ نوع قابل للإسناد.
flowchart LR
subgraph Before[قبل الاستدعاء]
A1[A = 10]
B1[B = 20]
end
A1 --> T[Temp = A]
B1 --> A2[A = B]
T --> B2[B = Temp]
subgraph After[بعد الاستدعاء]
A2[A = 20]
B2[B = 10]
end
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).
في الكومة، طالما تغيّر نوع العنصر وحجمه الأقصى فقط، يبقى المنطق الأساسيّ واحداً.
flowchart TB
G[Generic_Stack] --> P1[Element_Type]
G --> P2[Max_Size]
G --> Ops[Push / Pop / Size / Is_Empty / Is_Full]
G --> I1[Int_Stack<br/>Element=Integer<br/>Max_Size=5]
G --> I2[Float_Stack<br/>Element=Float<br/>Max_Size=3]
G --> I3[String_Stack<br/>Element=Unbounded_String<br/>Max_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);
هذان أُنشئا من القالب نفسه، لكنّهما لا يتشاركان الحالة الداخليّة.
flowchart LR
Template[Generic_Stack] --> IntStack[Int_Stack]
Template --> FloatStack[Float_Stack]
subgraph I[حالة Int_Stack]
ITop[Top]
IData[Data: مصفوفة Integer]
end
subgraph F[حالة Float_Stack]
FTop[Top]
FData[Data: مصفوفة Float]
end
IntStack --> I
FloatStack --> F
7.3 انتقالات حالة الكومة
يسهل فهم الكومة إذا نُظر إليها كآلة حالات.
stateDiagram-v2
Empty: فارغة
NonEmpty: غير فارغة
Full: ممتلئة
Overflow: فيضان
Underflow: نقصان
[*] --> Empty
Empty --> NonEmpty: Push
NonEmpty --> NonEmpty: Push / Pop
NonEmpty --> Empty: Pop يزيل العنصر الأخير
NonEmpty --> Full: Push يصل إلى Max_Size
Full --> NonEmpty: Pop
Full --> Overflow: Push
Empty --> Underflow: Pop
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، يمكن جعل دالّة المقارنة هذه معطى فرعيّاً للجينيريك.
flowchart LR
A[Generic_Insertion_Sort] --> T[Item_Type]
A --> I[Index]
A --> ARR[Item_Array]
A --> CMP[دالّة المقارنة]
CMP --> ASC[استخدام المقارنة المعياريّة]
CMP --> DESC[تمرير Greater للترتيب التنازليّ]
CMP --> CUSTOM[تمرير ترتيب مخصَّص]
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);
هنا أربعة معطيات فرعيّة.
Item_Type: نوع عنصر المصفوفةIndex: نوع فهرس المصفوفةItem_Array: نوع المصفوفة الفعليّ"<": دالّة المقارنة
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;
الترتيب بالإدراج لا يناسب المصفوفات الكبيرة، لكنّه مناسب لشرح الجينيريكس. لأنّه بمجرّد استبدال دالّة المقارنة فقط، يمكن استخدام بنية الحلقة نفسها للترتيب التصاعديّ أو التنازليّ على حدٍّ سواء.
flowchart TB
Start[مصفوفة غير مرتَّبة] --> Pick[أخذ Key من اليسار]
Pick --> Compare{هل يسبق Key العنصر السابق؟}
Compare -->|نعم| Shift[إزاحة العنصر السابق يميناً]
Shift --> Compare
Compare -->|لا| Insert[إدراج Key]
Insert --> Done{هل وصلنا إلى النهاية؟}
Done -->|لا| Pick
Done -->|نعم| End[مصفوفة مرتَّبة]
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.
flowchart LR
Data[99, 3, 47, 12] --> A[Sort_Asc<br/>المقارنة المعياريّة]
Data --> D[Sort_Desc<br/>يمرِّر Greater كمقارنة]
A --> AO[3, 12, 47, 99]
D --> DO[99, 47, 12, 3]
هذه الآليّة قريبة من تصميم تمرير كائن دالّة المقارنة كمعطى قالب في C++، أو تصميم طلب الترتيب عبر حدود السمات (trait bounds) في Rust. غير أنّ Ada تُصرِّح بوضوح، كمعطى فرعيّ للإجراء، بأنّه «يُمرَّر دالّة بهذا الشكل».
9. فئات الأنواع ── كتابة عقد أكثر تحديداً من private
type T is private; مفيد، لكنّه لا يستطيع كلّ شيء. بالنسبة لنوع private، لا يمكن استخدام العمليّات الحسابيّة الأربع أو المقارنة الكبرى/الصغرى بشكل بديهيّ. لذلك تسمح Ada بتحديد فئة (category) للمعطى الفرعيّ للنوع.
flowchart TB
FormalType[معطى النوع] --> Private[private]
FormalType --> Limited[limited private]
FormalType --> Discrete["discrete box: نوع منفصل (discrete)"]
FormalType --> Signed[range box: نوع صحيح موقَّع]
FormalType --> Modular["mod box: نوع صحيح إجرائيّ (modular)"]
FormalType --> Float[digits box: نوع فاصلة عائمة]
FormalType --> Fixed[delta box: نوع فاصلة ثابتة]
FormalType --> Access[نوع access]
Discrete --> Enum[نوع تعداديّ]
Discrete --> Int[نوع صحيح]
Float --> F1[Float]
Float --> F2[Long_Float]
Float --> F3[نوع فاصلة عائمة معرَّف من المستخدم]
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);
يمكن إعادة استخدام المعالجة الإحصائيّة نفسها لأنواع فاصلة عائمة مختلفة الدقّة.
flowchart LR
Stats[Generic_Statistics<br/>Real is digits box] --> FS[Float_Stats]
Stats --> LS[Long_Stats]
Stats --> MS[My_Real_Stats]
FS --> FCalc[Mean / Variance مع Float]
LS --> LCalc[Mean / Variance مع Long_Float]
MS --> MCalc[Mean / 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;
تدفّق المعالجة بسيط.
flowchart LR
Arr[مصفوفة] --> Loop[تصفّح كلّ عنصر]
Loop --> P{"Predicate(Item)?"}
P -->|True| Inc[زيادة Count]
P -->|False| Skip[لا تفعل شيئاً]
Inc --> Next[العنصر التالي]
Skip --> Next
Next --> Result[إرجاع 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);
من منطق التصفّح نفسه، يمكن إنشاء دالّتين تختلفان في الشرط فقط.
flowchart TB
G[Generic_Count_If] --> E[Count_Even<br/>Predicate = Is_Even]
G --> L[Count_Large<br/>Predicate = Is_Large]
Data[12, 7, 88, 3, 56, 91, 44, 19, 62] --> E
Data --> L
E --> ER[عدد الأعداد الزوجيّة]
L --> LR[العدد الذي يفوق 50]
في هذا المثال، تصفّح المصفوفة، وإدارة العدّاد، وإرجاع النتيجة، كلّها مشتركة. أمّا «ما الذي نعدّه» فيُحقَن كدالّة فقط. هذا هو الشكل الأساسيّ للتصميم عالي الرتبة في Ada.
11. تركيب معطيات متعدّدة ── مخزن مفتاح-قيمة عامّ
في المكوِّنات الواقعيّة، نادراً ما يكفي معطى نوع واحد فقط. يلزم دمج عدّة شروط، مثل نوع المفتاح والقيمة، وطريقة مقارنة المفتاح، والعدد الأقصى.
هنا نأخذ مثالاً لمخزن مفتاح-قيمة بسيط ثابت الطول.
flowchart TB
KV[Generic_KV_Store] --> K[Key_Type]
KV --> V[Value_Type]
KV --> EQ[دالّة تطابق المفتاح]
KV --> M[Max_Entries]
KV --> Ops[Put / Get / Contains]
Ops --> Use1[مخزن إعدادات]
Ops --> Use2[ذاكرة تخزين مؤقّت صغيرة الحجم]
Ops --> Use3[قاموس ثابت الحجم للاستخدام في الأنظمة المدمجة]
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)، فهو لا يناسب البيانات الكبيرة. لكن في المواضع التي تكون فيها خاصّيّة «ثابت الطول، صغير الحجم، بلا حجز ذاكرة ديناميكيّ» مهمّة، هذا شكل سهل الاستخدام.
sequenceDiagram
participant App as المُستدعي
participant Store as نسخة Generic_KV_Store
participant Data as Keys/Values/Used
App->>Store: Put(Key, Value)
Store->>Data: Find_Index(Key)
alt مفتاح موجود
Store->>Data: Values(Pos) := Value
else مفتاح جديد
Store->>Data: Find_Free
Store->>Data: Keys(Pos) := Key
Store->>Data: Values(Pos) := Value
Store->>Data: Used(Pos) := True
end
App->>Store: Get(Key)
Store->>Data: Find_Index(Key)
Data-->>Store: Pos
Store-->>App: Values(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 جعل الحزمة نفسها معطى فرعيّاً. باستخدام هذا، يمكن التعامل مع «نسخة أُنشئت من حزمة جينيريك معيّنة» كمدخل لجينيريك آخر.
flowchart LR
GS[Generic_Stack] --> IS[Int_Stack]
IS --> Logger[Generic_Stack_Logger]
Logger --> Logged[Int_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);
بهذا التصميم، يمكن دمج مكوِّنات الجينيريك مع بعضها.
flowchart TB
subgraph Layer1[المرحلة 1]
T1[Generic_Stack] --> I1[Int_Stack]
end
subgraph Layer2[المرحلة 2]
T2[Generic_Stack_Logger] --> I2[Int_Stack_Logger]
end
I1 --> T2
I2 --> API[Print_Size]
هذا الاستخدام قريب من معطيات قالب القالب (template template parameters) في C++، لكنّ Ada تُصرِّح بوضوح بـ«استقبال نسخة من هذه الحزمة العامّة». في شيفرة Ada الكبيرة، هذا مفيد عند فصل الحاويات، والخوارزميّات، والسجلّ، والفحص، وأدوات الاختبار المساعدة، ودمجها معاً.
13. نموذج العقد (Contract Model) ── أهمّ فكرة في جينيريكس Ada
من المهمّ لفهم جينيريكس Ada استيعاب نموذج العقد (contract model).
يجب كتابة جسم الجينيريك باستخدام العمليّات التي وعد بها المعطى الفرعيّ فقط. على سبيل المثال، إذا أُعلن فقط type Element is private;، لا يمكن استخدام < مع Element. إن أردنا استخدام <، يجب التصريح به صراحةً كإجراء فرعيّ، أو جعل فئة النوع أكثر تحديداً.
flowchart TB
Spec[الجزء الفرعيّ للجينيريك<br/>العقد] --> Body[جسم الجينيريك<br/>يُنفَّذ ضمن العقد]
Body --> Check1[يُفحَص نوع الجسم وحده]
Spec --> Inst[إنشاء النسخة]
Actual[المعطيات الفعليّة<br/>نوع/دالّة/قيمة فعليّة] --> Inst
Inst --> Check2[يتحقّق من وفاء المعطيات الفعليّة بالعقد]
Check2 --> Instance[حزمة أو إجراء عاديّ]
بهذا التصميم، يُحمى ليس فقط مستخدم الجينيريك، بل من يكتب الجينيريك أيضاً.
13.1 الفارق في المظهر مع قوالب C++
قوالب C++ قويّة، لكن تاريخيّاً كانت لها خاصّيّة «لا يظهر الخطأ إلا بعد إنشاء نسخة جسم القالب». تحسَّن هذا مع concepts في C++20، لكنّ جينيريكس Ada نموذج يُصرِّح بالعقد صراحةً منذ البداية.
flowchart LR
subgraph Ada[Ada]
A1[كتابة العقد في الجزء الفرعيّ] --> A2[يُفحَص الجسم ضمن العقد]
A2 --> A3[يتحقّق إنشاء النسخة من المعطيات الفعليّة]
end
subgraph CPP[قوالب C++]
C1[كتابة جسم القالب] --> C2[تظهر التعبيرات المطلوبة عند إنشاء النسخة]
C2 --> C3[يمكن لـconcepts توضيح القيود صراحةً]
end
جينيريكس Java وC# يتمحور تصميمها حول الأنواع المرجعيّة، والقيود، ومحو الأنواع (type erasure)، وعلاقتها بالتمثيل في وقت التشغيل. أمّا جينيريكس Ada فتميل نحو فكرة إنشاء نسخة محدَّدة في وقت الترجمة.
| المنظور | Ada | C++ | Java | Rust |
|---|---|---|---|---|
| طريقة كتابة العقد | كتابة النوع والقيمة والدالّة والحزمة في الجزء الفرعيّ | templates / concepts | معطى نوع وحدود (bounds) | حدود السمات (trait bounds) |
| فحص الجسم | يُفحَص ضمن عقد المعطى الفرعيّ | يتمحور حول التجسيد وقت الإنشاء | يُفحَص ضمن الحدود | يُفحَص ضمن حدود السمات |
| التكلفة في وقت التشغيل | الحلّ الساكن هو الأساس | التوليد الساكن هو الأساس | يتأثّر بمحو الأنواع | أحاديّة الشكل هي الأساس |
| معطى القيمة | موجود | موجود | محدود | جينيريكس ثابتة (const generics) |
| جعل الإجراء الفرعيّ معطى فرعيّاً | موجود | يُعبَّر عنه بكائنات الدوال وغيرها | لامدا/واجهات دالّيّة | إغلاقات (closures)/دوال/سمات (traits) |
| جعل الحزمة معطى فرعيّاً | موجود | معطيات قالب القالب وغيرها | غير موجود | يختلف عن بنية الوحدات (modules) |
تختلف تفاصيل ميّزات كلّ لغة، لكنّ سمة Ada هي «كتابة العقد كصياغة أوّلاً».
14. قرارات التصميم في العمل الفعليّ ── ما الذي يجب أن يصبح جينيريكاً
الجينيريكس مفيدة، لكن هذا لا يعني أنّ كلّ شيء يجب أن يصبح جينيريكاً. في العمل الفعليّ، يقلّ الفشل إذا اتُّبع الحُكم التالي:
flowchart TB
Start[توجد منطقيّة تريد إعادة استخدامها] --> Q1{هل يختلف النوع فقط؟}
Q1 -->|نعم| GType[فكِّر في معطى نوع]
Q1 -->|لا| Q2{هل يختلف الحجم أو العتبة أيضاً؟}
Q2 -->|نعم| GObject[أضف معطى قيمة]
Q2 -->|لا| Q3{هل يختلف سلوك المقارنة أو المُسنَد؟}
Q3 -->|نعم| GSubp[أضف إجراءً فرعيّاً]
Q3 -->|لا| Q4{هل تريد حمل حالة داخليّة أو واجهة برمجيّة معاً؟}
Q4 -->|نعم| GPackage[حزمة عامّة]
Q4 -->|لا| Normal[يكفي إجراء عاديّ]
14.1 المواضع المناسبة للإجراء العامّ
يناسب الإجراء العامّ الخوارزميّات التي لا تحمل حالة.
SwapSortCount_IfFind- تحويل من نوع
Map Min/Max
عندما يكون جسم الخوارزميّة قصيراً والمُدخَل والمُخرَج واضحين، يكون الإجراء أسهل قراءةً من الحزمة.
14.2 المواضع المناسبة للحزمة العامّة
تناسب الحزمة العامّة المواضع التي نريد فيها حمل عدّة عمليّات وحالة داخليّة مع النوع معاً.
- كومة ثابتة الطول
- مخزن مؤقّت حلقيّ (ring buffer)
- قاموس صغير الحجم
- مجموعة معالجة إحصائيّة
- تجريد إدخال/إخراج لكلّ جهاز
- مجموعة عمليّات لنوع رقميّ مزوَّد بوحدة قياس
في Ada خصوصاً، بما أنّ جزء مواصفة الحزمة هو الواجهة العامّة، وجسم الحزمة هو التنفيذ، يمكن استخدام الحزمة العامّة كـ«قالب وحدة آمن نوعيّاً».
flowchart LR
Spec[مواصفة الحزمة<br/>الواجهة البرمجيّة العامّة] --> User[المُستدعي]
Body[جسم الحزمة<br/>التنفيذ الداخليّ] -.مخفيّ.-> User
Formal[الجزء الفرعيّ للجينيريك<br/>عقد النوع/القيمة/الدالّة] --> Spec
Formal --> Body
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;
flowchart LR
Need[الجسم يحتاج إلى مقارنة القيم] --> Contract[أضف دالّة مقارنة إلى الجزء الفرعيّ]
Contract --> OK[يُتحقَّق من إمكانيّة المقارنة عند إنشاء النسخة]
Need --> NoContract[private فقط]
NoContract --> Error[خطأ ترجمة في جسم الجينيريك]
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 كاستثناءين مختلفين. إن أردنا التعامل معهما كاستثناء مشترك، فمن الجدير النظر في تصميم يُعرِّف الاستثناء خارج الجينيريك.
flowchart TB
Generic[Generic_Stack<br/>يُعلِن Stack_Overflow] --> I[Int_Stack.Stack_Overflow]
Generic --> F[Float_Stack.Stack_Overflow]
I -.استثناء مختلف.-> F
15.4 قد يزداد حجم الشيفرة
بينما تسهِّل الجينيريكس تجنّب الاستدعاء غير المباشر الزائد في وقت التشغيل، فإنّها بما أنّها تُنشئ نسخة لكلّ نوع، قد يزداد حجم الشيفرة إذا كثر عدد النسخ.
هذه مفاضلة (trade-off) تظهر أيضاً في قوالب C++ وأحاديّة الشكل (monomorphization) في Rust. في التطوير الموجَّه نحو الموثوقيّة العالية والأنظمة المدمجة وأنظمة الوقت الفعليّ، يصبح المنهج هو إدارة حجم مُخرجات البناء مقابل تقليل عدم اليقين في وقت التشغيل.
flowchart LR
Generic[جسم جينيريك واحد] --> I1[نسخة Integer]
Generic --> I2[نسخة Float]
Generic --> I3[نسخة Long_Float]
Generic --> I4[نسخة My_Type]
I1 --> Code[الشيفرة المُولَّدة]
I2 --> Code
I3 --> Code
I4 --> Code
Code --> Pros["يسهل تجنّب فحص الأنواع والتغليف (boxing) وقت التشغيل"]
Code --> Cons[انتبه لتضخّم الحجم مع زيادة عدد النسخ]
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;
flowchart LR
T[Element] --> C[يحتاج إلى دالّة مقارنة]
C --> M[Generic_Min]
M --> R[يُرجِع القيمة الأصغر]
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;
بهذا الشكل، يمكن استبدال وجهة الإخراج بين الطرفيّة، والسجلّ، والمخزن المؤقّت الخاصّ بالاختبار وغيرها.
flowchart TB
Print[Generic_Print_Twice] --> Put[يستقبل Put كإجراء فرعيّ]
Put --> Console[إخراج الطرفيّة]
Put --> Log[إخراج السجلّ]
Put --> Test[مخزن مؤقّت للاختبار]
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، بل مع المصفوفات ذات فهرس تعداديّ أيضاً.
flowchart LR
Index["Index نوع منفصل (discrete)"] --> Positive[مجال Positive]
Index --> Day[تعداد Day]
Index --> State[تعداد State]
Index --> Slot[نوع صحيح مخصَّص]
17. قائمة تحقّق لجعل الواجهة البرمجيّة (API) أقرب لأسلوب Ada
عند كتابة جينيريك، تصبح أسهل قراءةً إذا أُعيد النظر فيها من الزوايا التالية في النهاية.
flowchart TB
C[قائمة تحقّق لتصميم الجينيريك] --> C1[هل المعطيات الفرعيّة في حدّها الأدنى؟]
C --> C2[هل العمليّات المطلوبة صريحة في الجزء الفرعيّ؟]
C --> C3[هل الفئات مثل private / range / digits مناسبة؟]
C --> C4[هل يمكن كتابة إنشاء النسخة بوضوح باستخدام الارتباط المُسمَّى؟]
C --> C5[هل رُوعِيت الحالة والاستثناءات الخاصّة بكلّ نسخة؟]
C --> C6[هل تضخّم حجم الشيفرة مقبول؟]
C --> C7[هل تتوفّر نسخة اختباريّة؟]
نلخِّصها نصّيّاً كالتالي:
- اجعل العمليّات المُستخدَمة في الجسم مرئيّة دائماً كعقد للمعطى الفرعيّ.
- إن كفى
privateفاستخدمه، وإن لزم الحساب فاستخدمrange <>أوdigits <>. - اجعل السلوك الذي يختلف باختلاف النوع، كالمقارنة والتجزئة (hash) والإخراج والتحويل، إجراءً فرعيّاً.
- إن كان الحجم أو العتبة خاصّيّة للنسخة، فاجعله معطى قيمة.
- إن حمل حالة فابدأ بالحزمة العامّة، وإن لم يحمل حالة فابدأ بالإجراء العامّ.
- في إنشاء النسخة، كلّما زاد عدد المعطيات، استخدم الارتباط المُسمَّى.
- صمِّم على افتراض أنّ الاستثناءات والحالة الداخليّة تستقلّ لكلّ نسخة.
18. مثال بنية كاملة للأمثلة
إذا فُصلت الأمثلة الواردة في المقال إلى ملفّات، فإنّ البنية التالية تجعل القراءة أسهل.
flowchart TB
Root[ada-generic-programming] --> Src[src]
Src --> G[generics]
Src --> D[demos]
G --> SwapSpec[generic_swap.ads]
G --> SwapBody[generic_swap.adb]
G --> StackSpec[generic_stack.ads]
G --> StackBody[generic_stack.adb]
G --> SortSpec[generic_insertion_sort.ads]
G --> SortBody[generic_insertion_sort.adb]
G --> StatsSpec[generic_statistics.ads]
G --> StatsBody[generic_statistics.adb]
G --> CountSpec[generic_count_if.ads]
G --> CountBody[generic_count_if.adb]
G --> KVSpec[generic_kv_store.ads]
G --> KVBody[generic_kv_store.adb]
D --> SwapDemo[swap_demo.adb]
D --> StackDemo[stack_demo.adb]
D --> SortDemo[sort_demo.adb]
D --> StatsDemo[statistics_demo.adb]
D --> CountDemo[count_if_demo.adb]
D --> KVDemo[kv_demo.adb]
بالنسبة للأمثلة الصغيرة الخاصّة بالمقالات، من المريح تجميعها في ملفّ واحد وتقسيمها عبر gnatchop، لكن عند التفكير في العمل الفعليّ أو الصيانة طويلة الأمد، فإنّ الفصل بين المواصفة .ads والجسم .adb بنية أقرب لأسلوب Ada.
19. الخلاصة ── تحديد حدود إعادة الاستخدام بالأنواع
البرمجة العامّة في Ada ليست مجرّد «ميّزة لكتابة شيفرة لا تعتمد على النوع». بل جوهرها التصريح بما يتطلّبه المكوِّن القابل لإعادة الاستخدام، كعقد بالأنواع والإجراءات الفرعيّة والقيم.
flowchart LR
Contract[اكتب العقد] --> Generic[اكتب جسم الجينيريك]
Generic --> Instance[أنشئ نسخة بتمرير نوع/قيمة/دالّة]
Instance --> Safe[استخدمه بأمان نوعيّ]
Safe --> Reuse[أعد استخدامه دون نسخ]
كما رأينا في هذا المقال، يمكن في جينيريكس 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 القائمة من أجل الإطالة أو الترحيل، موضوع قريب في العمل الفعليّ أيضاً.
- تطوير تطبيقات Windows
- تطوير مكوِّنات COM
- استبدال تطبيقات Windows
- الاستشارة التقنيّة ومراجعة التصميم
- التواصل معنا
روابط مرجعيّة
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
سحر لغة Ada ── لغة تُعبِّر عن التصميم بالأنواع، وتدعم برمجيّات تعمل لعقود
نقدّم سحر لغة Ada. الأنواع القويّة، وقيود المدى، والفصل بين المواصفة والتنفيذ عبر الحزم، والتصميم بالعقد، والمهامّ المدمجة في اللغة، والت...
برمجة أنظمة الوقت الفعليّ بلغة Ada ── التطبيق العمليّ للتحكّم في الأولويّة والدوريّة ووقت التنفيذ
نتعلّم الملحق D (أنظمة الوقت الفعليّ) في Ada عبر ثمانية أمثلة برمجيّة تطبيقيّة. نرتِّب تدريجيّاً أولويّة المهامّ، وبروتوكول Ceiling_Locki...
التزامن الآمن في Ada ── دليل عملي للمهامّ (Tasks) والكائنات المحمية (Protected Objects)
مقال تمهيدي عن التزامن المدمج في لغة Ada عبر المهامّ (tasks) والكائنات المحمية (protected objects). نستعرض الرندفو (entry/accept)، والقبو...
مدخل إلى التحقق الشكلي باستخدام SPARK ── من عقود Ada إلى البرهان الرياضي
مقال تطبيقيّ يقدّم مدخلاً إلى التحقق الشكلي باستخدام SPARK، مجموعة الأداة الفرعيّة للغة Ada. يتناول المقال الانتقال من العقود (Pre/Post) ...
معالجة الأخطاء وتصميم إعادة المحاولة في Power Automate ── منع «توقّف تدفّق كان يعمل دون أن يلاحظ أحد»
مجموعة أنماط تصميميّة لمنع تدفّقات Power Automate من «التوقّف دون أن يلاحظ أحد». نستعرض، استناداً إلى مواصفات Microsoft Learn، القيم الاف...
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- ما هي جينيريكس 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) لحزمة عامّة يصبح استثناءً مختلفاً لكلّ نسخة. الإرشاد العمليّ هو البدء بمعطيات فرعيّة قليلة، وزيادتها عندما يظهر سبب يستدعي الاستبدال، واستخدام الارتباط المُسمَّى عند وجود عدد كبير من المعطيات في الإنشاء.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة