سحر لغة Ada ── لغة تُعبِّر عن التصميم بالأنواع، وتدعم برمجيّات تعمل لعقود

· آخر تحديث: · · Ada, لغة برمجة, الأنواع القويّة, SPARK, GNAT, Alire, النزاهة العالية, الأنظمة المدمجة, الموثوقية العالية

1. ما ينبغي فهمه أوّلاً

هل سمعتَ باسم لغة Ada من قبل؟

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

لكنّ Ada لغة ما زالت حيّة وفعّالة اليوم.

في عالم البرمجيّات التي يتّصل توقّفها بأرواح البشر ── التحكّم في طيران الطائرات، وأنظمة الإشارة في السكك الحديديّة، والصواريخ، ومراقبة الحركة الجويّة، والأقمار الصناعيّة، والأجهزة الطبّيّة ── ما زالت Ada مستخدَمة منذ عقود.

عند محاولة فهم Ada، من المهمّ تبنّي المنظور التالي:

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

في هذا المقال، نستعرض تاريخ Ada، وصياغتها، والأنواع القويّة، وقيود المدى، والحزم، والتصميم بالعقد، والمهامّ، وSPARK، وبيئة التطوير، وحتّى نقاط ضعفها، ونجمع سحر هذه اللغة.

نهدف إلى أن يخرج من يكتب عادةً بلغات مثل C# وC++ وJava بحسٍّ جديد عن معنى «التعبير عن التصميم بالأنواع».

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

ada-language-appeal - komurasoft-blog-samples (GitHub)

2. ما هي Ada ── أصل الاسم وتاريخها

Ada لغة برمجة عامّة الغرض، وُلدت في أواخر السبعينيّات بمبادرة من وزارة الدفاع الأمريكيّة (DoD).

كانت وزارة الدفاع آنذاك تعاني من مشكلة تفاقُم تكاليف صيانة البرمجيّات بسبب استخدام لغات مختلفة في كلّ مشروع.

لذا اختارت، عبر مسابقة تصميم دوليّة، لغة موحَّدة يمكن استخدامها أيضاً في الأنظمة المدمجة وأنظمة الوقت الفعليّ.

وقع الاختيار على تصميم فريق بقيادة Jean Ichbiah.

اسم اللغة Ada مأخوذ من Ada Lovelace (أوغستا إيدا كينغ، كونتيسة لوفليس)، التي تُلقَّب بأوّل مبرمِجة في العالم.

يمكن تلخيص تاريخ Ada بإيجاز على النحو التالي:

1980 يُصدَر أوّل مواصفة باسم MIL-STD-1815
1983 Ada 83 (معيار ANSI)
1987 تصبح معياراً دوليّاً ISO
1995 Ada 95 (إدخال البرمجة كائنيّة التوجّه، والكائنات المحميّة)
2005 Ada 2005 (الواجهات، وتوسيع مكتبة الحاويات)
2012 Ada 2012 (إدخال التصميم بالعقد كميّزة لغويّة)
2022 Ada 2022 (المعيار الأحدث)

كانت Ada 95 من أوائل اللغات كائنيّة التوجّه التي وُحِّدت معياريّاً عبر ISO.

وفي Ada 2012، أُدمِج التصميم بالعقد (Design by Contract) ── من شروط سابقة، وشروط لاحقة، وثوابت النوع ── في مواصفة اللغة نفسها.

Ada ليست «لغة قديمة»، بل هي لغة استمرّت مراجعتها لأكثر من 40 عاماً.

3. أين تُستخدَم Ada

المجالات الرئيسيّة التي ما زالت Ada مستخدَمة فيها هي الأنظمة التي تتطلّب موثوقيّة عالية (High Integrity).

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

تشترك هذه المجالات في خصائص واحدة.

الخطأ يتّصل مباشرةً بأرواح البشر أو بخسائر ضخمة
تُطلَب «أدلّة على الصحّة» عند الاعتماد أو التدقيق
بمجرّد النشر، تُستخدَم لعقود
تكلفة الإصلاح لاحقاً مرتفعة للغاية

فكرة «نُصلِح بعد الإطلاق» لا تصلح في هذا العالم.

تصميم لغة Ada مُوجَّه بالضبط لتلبية هذا المتطلّب.

الفلسفة التي تسري في اللغة كاملة هي: ما يمكن كشفه في وقت الترجمة يُكشَف في وقت الترجمة، وما لا يمكن كشفه إلا في وقت التشغيل يُكشَف بفحوصات وقت التشغيل، بل وما يمكن إثباته رياضيّاً يُثبَت رياضيّاً.

هذه الفلسفة تستحقّ التعلّم أيضاً من مطوّري تطبيقات الأعمال على الويب أو سطح المكتب.

4. لنبدأ بـ Hello, World

لنُلقِ نظرة على شيفرة Ada.

with Ada.Text_IO;

procedure Hello is
begin
   Ada.Text_IO.Put_Line ("Hello, Ada!");
end Hello;

أوّل ما يلفت الانتباه هو النقاط التالية:

تُستورَد وحدة المكتبة عبر with
جسم البرنامج هو إجراء (procedure)
تُحاط الكتلة بـ begin / end
يُعاد ذكر الاسم بعد end
تنتهي الجُمل بفاصلة منقوطة

من سمات Ada إعادة كتابة الاسم عند النهاية، كما في end Hello;.

حتّى عندما تتعمّق الكتل، يتّضح بلمحة واحدة «إلى أيّ end ينتمي هذا end».

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

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

5. صياغة تُعطي الأولويّة لسهولة القراءة

صُمِّمت صياغة Ada بإعطاء الأولويّة لسهولة القراءة على سهولة الكتابة.

لأنّها تقف على افتراض أنّ البرمجيّات تُقرأ أكثر بكثير ممّا تُكتب.

على سبيل المثال، تُكتَب الحلقات والتفريعات الشرطيّة على النحو التالي.

for I in 1 .. 5 loop
   Ada.Text_IO.Put_Line (Integer'Image (I * I));
end loop;

if Temperature > 80.0 then
   Start_Cooling;
elsif Temperature < 20.0 then
   Start_Heating;
else
   Keep_Current_State;
end if;

لجملة case سمة مميّزة لِـ Ada.

case Today is
   when Mon .. Fri =>
      Put_Line ("Weekday");
   when Sat | Sun =>
      Put_Line ("Weekend");
end case;

النقاط الأساسيّة هي التالية:

case يتحوّل إلى خطأ في وقت الترجمة إن لم يغطِّ جميع القيم
لا وجود لِـ fallthrough الضمنيّ كما في لغات عائلة C
يمكن تجميع الشروط عبر المدى (Mon .. Fri) أو الخيارات (Sat | Sun)

عند إضافة قيمة إلى نوع تعدادي، تتحوّل كلّ جملة case التي لا تغطّي هذه القيمة إلى خطأ في وقت الترجمة.

تجربة «المترجم يُعدِّد لك مواضع تأثير تغيير المواصفات» تجربة لا يمكن الاستغناء عنها بعد تذوّقها مرّة.

كذلك، يمكن استخدام الارتباط المُسمَّى في المعطيات (arguments).

Draw_Rectangle (Left => 10, Top => 20, Width => 100, Height => 50);

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

الإسناد بـ := والمقارنة بـ =، فلا يحدث خلط على مستوى الصياغة كما في if (a = b) في لغات عائلة C.

6. الأنواع القويّة ── تحويل الخلط بين الوحدات إلى خطأ في وقت الترجمة

أعظم سحر في Ada هو الأنواع القويّة (Strong Typing).

تُستخدَم عبارة «أنواع قويّة» في لغات كثيرة، لكنّ الأمر في Ada أعمق بدرجة.

في Ada، حتّى لو تطابقت البنية تماماً، فإنّ النوعين المُعلَنين باسمين مختلفين نوعان مختلفان.

type Meters  is new Float;
type Seconds is new Float;

Distance : Meters  := 100.0;
Time     : Seconds := 9.58;

كلا هذين في الجوهر أرقام عشريّة، لكن لا يمكن خلطهما معاً.

Distance := Time;            -- خطأ في وقت الترجمة
Distance := Distance + Time; -- خطأ في وقت الترجمة

يُكتَب التحويل المقصود صراحةً فقط.

Speed : constant Float := Float (Distance) / Float (Time);

لماذا هذه الصرامة إلى هذا الحدّ؟

ليست قلّةً حوادث برمجيّات العالم الحقيقيّ الناجمة عن «الخلط بين الوحدات».

من الأمثلة الشهيرة، فُقِد مسبار المريخ Mars Climate Orbiter عام 1999 بسبب خلط نظام الياردة والرطل بالنظام المتريّ.

جواب Ada بسيط.

اجعل المتر والقدم نوعين مختلفين
اجعل الخلط بينهما خطأً في وقت الترجمة
اجعل التحويل يُكتَب صراحةً

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

هذا هو الموقف الأساسيّ لِـ Ada.

7. قيود المدى ── منع القيم غير الصحيحة على مستوى نوع البيانات

يمكن في Ada منح النوع مدىً للقيمة.

subtype Percentage is Integer range 0 .. 100;

Progress : Percentage := 50;

عند محاولة وضع قيمة خارج المدى في متغيّر من نوع Percentage، يحدث استثناء Constraint_Error في وقت التشغيل.

Progress := 120;  -- Constraint_Error في وقت التشغيل

أمّا المخالفات التي يمكن معرفتها في وقت الترجمة، فتُكشَف في وقت الترجمة.

يمكن التعبير عن افتراضات ضمنيّة مثل «قيمة يُفترَض أنّها بين 0 و100» أو «قيمة يُفترَض أنّها 1 فأكثر» بالنوع، لا بالتعليق.

بالفعل، تحتوي المكتبة القياسيّة لِـ Ada على أنواع مقيَّدة شائعة الاستخدام مُعرَّفة مسبقاً.

Natural  = Integer range 0 .. Integer'Last
Positive = Integer range 1 .. Integer'Last

بل إنّه ابتداءً من Ada 2012، يمكن إرفاق أيّ شرط كمُسنَد (predicate).

subtype Even is Integer
  with Dynamic_Predicate => Even mod 2 = 0;

كما أنّ الأنواع الموجَّهة للتحكّم في العتاد، مثل الأنواع ذات الفاصلة الثابتة، مدمَجة في اللغة أيضاً.

type Temperature is delta 0.1 range -50.0 .. 150.0;

في لغات كثيرة، يميل التحقّق من القيم غير الصحيحة إلى أن يكون كالتالي.

التحقّق (validation) عبر جملة if في بداية الدالّة
سهولة إغفال التحقّق تُترَك لمراجعة الشيفرة
غموض حول أيّ دالّة تستقبل قيمة تمّ التحقّق منها بالفعل

في Ada، يمكن القول إنّه «بمجرّد أن تكون القيمة من هذا النوع، فإنّ المدى مضمون».

بانتقال مسؤوليّة التحقّق إلى نوع البيانات، يمكن لمنطق الدالّة أن يركِّز على عمله الأصليّ.

8. المصفوفات والفهارس ── التحقّق من الحدود والفهارس التعداديّة

يمكن في مصفوفات Ada اختيار نوع الفهرس بحرّيّة.

type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);

type Hours_Array is array (Day) of Natural;

Work_Hours : Hours_Array := (Mon .. Fri => 8, others => 0);

هذه مصفوفة فهرسها هو النوع التعداديّ Day.

يمكن الوصول إليها بصيغة Work_Hours (Wed)، دون حاجة لتذكّر «ماذا يعني الفهرس الرقميّ».

يمكن كتابة الحلقات أيضاً وفق نوع الفهرس.

for D in Work_Hours'Range loop
   Put_Line (Day'Image (D) & ":" & Natural'Image (Work_Hours (D)));
end loop;

بخصائص مثل 'Range وَ'First وَ'Last وَ'Length، يمكن الحصول على معلومات حدود المصفوفة في أيّ وقت.

بعدم كتابة الحدود بشكل ثابت (hardcode)، لا ينتشر تغيير حجم المصفوفة إلى الحلقات.

والمهمّ هو أنّ الوصول إلى المصفوفة يخضع دائماً لفحص الحدود.

Buffer : String (1 .. 10);
Index  : Integer := 11;

Buffer (Index) := 'x';  -- Constraint_Error في وقت التشغيل

يظلّ فيض المخزن المؤقّت (buffer overrun) في C/C++ السبب الرئيسيّ لثغرات أمنيّة منذ زمن طويل.

في Ada، الوصول خارج المدى ليس سلوكاً غير مُعرَّف (undefined behavior)، بل استثناء مُعرَّف.

بدلاً من إتلاف الذاكرة بصمت وإحداث انهيار غامض في مكان آخر، يتوقّف فوراً بضجّة كبيرة عند نقطة حدوث المشكلة.

هذا الفارق كبير جدّاً بالنظر إلى تكلفة تحقيق الأنظمة التي تُشغَّل لفترة طويلة.

9. الحزم ── الفصل بين المواصفة والتنفيذ

آليّة الوحدات (module) في Ada هي الحزمة (package).

تنقسم الحزمة إلى ملفّين: المواصفة (spec) والجسم (body).

counters.ads  المواصفة: الواجهة المُتاحة للخارج
counters.adb  الجسم: تفاصيل التنفيذ

تُكتَب المواصفة على النحو التالي.

package Counters is

   type Counter is private;

   procedure Increment (C : in out Counter);
   function  Value     (C : Counter) return Natural;

private
   type Counter is record
      Count : Natural := 0;
   end record;

end Counters;

ويُكتَب الجسم على النحو التالي.

package body Counters is

   procedure Increment (C : in out Counter) is
   begin
      C.Count := C.Count + 1;
   end Increment;

   function Value (C : Counter) return Natural is
   begin
      return C.Count;
   end Value;

end Counters;

النقاط التي نريد لفت الانتباه إليها هي التالية:

عند إعلان type بـ private، لا يلمس المستخدم البنية الداخليّة
قراءة المواصفة (.ads) وحدها تكفي لمعرفة طريقة الاستخدام كاملةً
تعديل الجسم (.adb) يُبقي إعادة ترجمة جهة الاستخدام في حدّها الأدنى ما دامت المواصفة كما هي

يشبه هذا ملفّات الترويسة (header) في C/C++، لكنّه ليس توسيعاً نصّيّاً كـ #include، بل يُفحَص اتّساقه كجزء من مواصفة اللغة نفسها.

أيّ تعارض بين المواصفة والجسم يتحوّل إلى خطأ في وقت الترجمة.

كما يجب كتابة نمط (mode) لكلّ معطى، وهو in أو out أو in out.

procedure Increment (C : in out Counter);

يمكن معرفة «هل هذا المعطى للقراءة فقط، أم للكتابة فقط، أم للقراءة والكتابة معاً» بمجرّد النظر إلى التوقيع (signature).

يمكن قراءة اتّجاه تدفّق البيانات دون الحاجة لمعرفة المؤشّرات أو المراجع.

10. السجلّات والمُميِّزات

ما يقابل البنية (struct) في Ada هو السجلّ (record).

type Point is record
   X : Float := 0.0;
   Y : Float := 0.0;
end record;

P : Point := (X => 1.0, Y => 2.0);

يمكن منح الحقول قيماً افتراضيّة، ويمكن التهيئة المُسمَّاة عبر التجميعة (aggregate).

الميزة الخاصّة بِـ Ada هي المُميِّز (discriminant).

type Buffer (Size : Positive) is record
   Data   : String (1 .. Size);
   Length : Natural := 0;
end record;

Small : Buffer (Size => 16);
Large : Buffer (Size => 4096);

المُميِّز هو معطى يحدِّد «شكل» السجلّ.

Buffer (16) وَBuffer (4096) من النوع نفسه، لكنّ حجم المصفوفة الداخليّة يتحدَّد وقت الإعلان ولا يتغيّر بعد ذلك.

يمكن تصوّره على أنّه إدارة آمنة كنوع، لما يُسمّى في C «بنية بعضو متغيّر الطول + حقل حجم».

عدم اتّساق الحجم مع الجسم الفعليّ، وهو خطأ شائع في C، غير موجود من الأساس.

11. الجينيريكس

تمتلك Ada الجينيريكس (generic) منذ معيارها الأوّل عام 1983.

وهذا أبكر بكثير من قوالب (templates) C++ (تسعينيّات القرن الماضي) أو جينيريكس Java (2004).

generic
   type Element is private;
procedure Swap (Left, Right : in out Element);

procedure Swap (Left, Right : in out Element) is
   Temp : constant Element := Left;
begin
   Left  := Right;
   Right := Temp;
end Swap;

يقوم المستخدم بإنشاء نسخة (instantiate) بنوع محدَّد.

procedure Swap_Integers is new Swap (Element => Integer);
procedure Swap_Floats   is new Swap (Element => Float);

سمة جينيريكس Ada هي التصريح الصريح بالعمليّات المطلوبة.

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

يُكتَب في المواصفة أنّ «هذه الدالّة العامّة تتطلّب من النوع Element عمليّة مقارنة».

المشكلة التي عانت منها قوالب C++ طويلاً، وهي «لا يظهر الخطأ إلا بعد الإنشاء (instantiation)»، لا تحدث في Ada من الأساس.

المشكلة التي حاولت concepts في C++20 وحدود السمات (trait bounds) في Rust حلّها، كانت Ada تملك إجابتها منذ 40 عاماً.

12. معالجة الاستثناءات

تمتلك Ada معالجة استثناءات.

with Ada.Text_IO;
with Ada.Exceptions;

procedure Read_Config is
begin
   Load_File ("config.txt");
exception
   when Ada.Text_IO.Name_Error =>
      Ada.Text_IO.Put_Line ("لم يُعثَر على ملفّ الإعدادات");
   when E : others =>
      Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Information (E));
      raise;
end Read_Config;

يُكتَب جزء exception في نهاية الكتلة، وتُصفّ المعالجات حسب نوع الاستثناء.

من الاستثناءات المُعرَّفة في اللغة، الأمثلة النموذجيّة هي التالية:

Constraint_Error  انتهاك قيد المدى، وانتهاك حدود المصفوفة، والقسمة على صفر وغيرها
Program_Error     انتهاك قواعد اللغة (كالوصول إلى مكان لا يجوز الوصول إليه)
Storage_Error     نفاد الذاكرة
Tasking_Error     فشل الاتّصال بين المهامّ

ما يستحقّ الانتباه هو أنّ انتهاكات قيود المدى وفحص الحدود مُدمَجة كلّها في آليّة الاستثناءات هذه.

عندما يُنتهَك «قيد كُتِب في النوع»، يحدث Constraint_Error.

بعبارة أخرى، فإنّ قيود المدى التي رأيناها في الفصل 7 تعمل كـتأكيدات (assertions) تُولَّد تلقائيّاً في وقت التشغيل.

لا حاجة لكتابة شيفرة تحقّق بجُمل if بنفسك بشكل متفرِّق.

13. التصميم بالعقد ── كتابة شروط Pre/Post كميّزة لغويّة

أبرز ميّزة في Ada 2012 هي الدعم اللغويّ لـالتصميم بالعقد (Design by Contract).

يمكن كتابة شرط سابق (Pre) وشرط لاحق (Post) مباشرةً على الإجراءات والدوال.

package Stacks is

   type Stack is private;

   function Is_Full  (S : Stack) return Boolean;
   function Is_Empty (S : Stack) return Boolean;
   function Count    (S : Stack) return Natural;

   procedure Push (S : in out Stack; Item : Integer)
     with Pre  => not Is_Full (S),
          Post => Count (S) = Count (S)'Old + 1;

   procedure Pop (S : in out Stack; Item : out Integer)
     with Pre  => not Is_Empty (S),
          Post => Count (S) = Count (S)'Old - 1;

private
   -- تفاصيل التنفيذ
end Stacks;

Pre هو «الوعد الذي يجب على جهة الاستدعاء الوفاء به»، وَPost هو «الوعد الذي يضمنه جانب التنفيذ».

يمكن الرجوع إلى القيمة قبل الاستدعاء عبر الخاصّيّة 'Old.

يمكن تفعيل هذا العقد كفحص في وقت التشغيل عبر خيار الترجمة (في GNAT هو -gnata).

عند انتهاك العقد، يحدث استثناء Assertion_Error، ويتّضح بوضوح مَن أخلّ بالوعد.

انتهاك Pre  -> خطأ في جهة الاستدعاء
انتهاك Post -> خطأ في جانب التنفيذ

ما الفرق بين هذا وبين كتابة «لا يجوز استدعاء هذه الدالّة على كومة (stack) فارغة» في تعليق توثيقيّ؟

التعليق قد ينحرف عن التنفيذ دون أن ينتبه أحد
العقد يفحصه المترجم من ناحية الصياغة والأنواع
يمكن التحقّق من العقد تلقائيّاً في وقت التشغيل
يصبح العقد مُدخَلاً للبرهان الساكن عبر SPARK (الفصل 16)

المواصفة موجودة داخل الشيفرة بصيغة يمكن التحقّق منها.

هذا هو عالَم Ada 2012 فما بعده.

باستخدام ثابت النوع (Type_Invariant)، يمكن أيضاً كتابة قيد من نوع «قيمة هذا النوع تحقِّق دائماً هذه الخاصّيّة».

14. المهامّ ── المعالجة المتوازية مدمَجة في اللغة

سحرٌ كبير آخر لِـ Ada هو أنّ المعالجة المتوازية جزء من مواصفة اللغة.

بينما تعتمد C/C++ على واجهات برمجيّة (API) أو مكتبات نظام التشغيل للخيوط (pthread وَstd::thread)، كانت Ada تمتلك المهامّ (task) مدمَجة في اللغة منذ عام 1983.

with Ada.Text_IO;

procedure Task_Demo is

   task Worker;

   task body Worker is
   begin
      for I in 1 .. 3 loop
         Ada.Text_IO.Put_Line ("worker:" & Integer'Image (I));
         delay 0.5;
      end loop;
   end Worker;

begin
   for I in 1 .. 3 loop
      Ada.Text_IO.Put_Line ("main  :" & Integer'Image (I));
      delay 0.5;
   end loop;
end Task_Demo;

عند إعلان task، يبدأ التنفيذ المتوازي بالتزامن مع بداية الكتلة المحيطة.

والمهمّ هو أنّ الكتلة لا تنتهي حتّى تنتهي جميع المهامّ الداخليّة.

نوع الخطأ الذي يحدث بنسيان الـ join للخيط ويؤدّي إلى حدوث شيء غريب عند إنهاء العمليّة، لا يحدث بنيويّاً.

للتزامن بين المهامّ آليّة لغويّة تُسمّى اللقاء (rendezvous).

task Logger is
   entry Write (Message : String);
end Logger;

task body Logger is
begin
   loop
      select
         accept Write (Message : String) do
            Ada.Text_IO.Put_Line (Message);
         end Write;
      or
         terminate;
      end select;
   end loop;
end Logger;

يكتب المستخدم Logger.Write ("hello");، بالشكل نفسه لاستدعاء إجراء عاديّ.

يمكن كتابة الاتّصال بين المهامّ عبر تمرير الرسائل دون الحاجة لمعرفة الأقفال (locks).

للأنظمة الموجَّهة للوقت الفعليّ، وُحِّد معياريّاً حتّى ملف تعريف Ravenscar الذي يقيِّد ميّزات المهامّ لسياسات الجدولة والتحكّم في الأولويّة، بل ولتسهيل التحقّق.

15. الكائنات المحميّة ── كتابة التحكّم الحصريّ كنوع

للتحكّم الحصريّ في البيانات المشتركة، تُستخدَم الكائنات المحميّة (protected object) التي أُدخِلت في Ada 95.

protected Shared_Counter is
   procedure Increment;
   function  Value return Natural;
private
   Count : Natural := 0;
end Shared_Counter;

protected body Shared_Counter is

   procedure Increment is
   begin
      Count := Count + 1;
   end Increment;

   function Value return Natural is
   begin
      return Count;
   end Value;

end Shared_Counter;

لا يمكن الوصول إلى بيانات الكائن المحميّ إلا عبر العمليّات المُعرَّفة.

والتحكّم الحصريّ تضمنه اللغة نفسها.

procedure  قراءة وكتابة، يُنفَّذ حصريّاً
function   قراءة فقط، يسمح بتنفيذ متزامن لعدّة مهامّ
entry      يمكنه إبقاء جهة الاستدعاء منتظرة حتّى تحقُّق شرط (barrier)

يميل التحكّم الحصريّ في لغات كثيرة إلى الاعتماد على انضباط من نوع:

احرص على أخذ هذا الـ mutex عند لمس هذه البيانات
لا تنسَ تحرير القفل
احترم ترتيب الأقفال

في الكائنات المحميّة لِـ Ada، «شيفرة نسيت أخذ القفل» لا يمكن كتابتها من الأساس.

لأنّ البيانات والتحكّم الحصريّ الذي يحميها يُعلَنان كنوع واحد.

باستخدام شرط الحاجز (barrier) في entry، يمكن أيضاً كتابة تزامن شرطيّ مثل «الانتظار حتّى تدخل بيانات إلى الطابور» دون إدارة يدويّة لأعلام أو متغيّرات شرط.

16. SPARK ── طريق نحو التحقّق الشكليّ

في عالَم Ada رفيق قويّ يُدعى SPARK.

SPARK مجموعة فرعيّة (subset) من Ada، مصمَّمة بحيث يمكن إثبات خصائص البرنامج رياضيّاً.

procedure Increment (X : in out Integer)
  with SPARK_Mode,
       Pre  => X < Integer'Last,
       Post => X = X'Old + 1;

تُثبِت أداة SPARK (GNATprove) للشيفرة أموراً كالتالي دون تنفيذها:

عدم حدوث فيض (overflow)
عدم حدوث انتهاك لقيد المدى
عدم حدوث قسمة على صفر
عدم قراءة متغيّر غير مُهيَّأ
اتّساق Pre وَPost

الفارق مع الاختبار حاسم.

الاختبار  يتحقّق من أنّ البرنامج يعمل بشكل صحيح للمدخلات المختارة
الإثبات   يُظهِر أنّ الخاصّيّة تتحقّق لكلّ المدخلات

العقد (Pre/Post) الذي رأيناه في الفصل 13 يصبح في SPARK هدفاً للإثبات مباشرةً.

يمكن ترقية العقد المكتوب كفحص في وقت التشغيل إلى «مُثبَت» لاحقاً.

راكمت SPARK إنجازاتها في عالَم الطيران والدفاع، لكن في السنوات الأخيرة، اتّسع استخدامها في الصناعة، مثل اعتماد NVIDIA لها لأمان البرامج الثابتة (firmware).

يستمرّ نظام Ada/SPARK البيئيّ بهدوء في قلب الفكرة السائدة القائلة بأنّ «الأساليب الشكليّة أكاديميّة أكثر من اللازم لتُستخدَم عمليّاً».

17. التشغيل البيني مع C وC++

Ada ليست لغة معزولة.

التشغيل البيني مع C موحَّد معياريّاً في مواصفة اللغة (الملحق B).

على سبيل المثال، لاستدعاء Sleep من واجهة برمجيّة تطبيقات (API) في Windows من Ada، يُكتَب كالتالي.

with Interfaces.C;

procedure Sleep_Demo is

   procedure Sleep (Milliseconds : Interfaces.C.unsigned)
     with Import,
          Convention    => Stdcall,
          External_Name => "Sleep";

begin
   Sleep (1000);
end Sleep_Demo;

النقاط الأساسيّة هي التالية:

Import           استيراد تنفيذ خارجيّ
Convention       تحديد اتّفاقيّة الاستدعاء (C، Stdcall وغيرها)
External_Name    تحديد اسم الرمز (symbol) عند الربط
Interfaces.C     توفير أنواع مقابلة لأنواع C (int، unsigned، char* وغيرها)

الاتّجاه المعاكس ممكن أيضاً.

باستخدام Export، يمكن نشر إجراء مكتوب بِـ Ada كدالّة تُستدعى من C.

بعبارة أخرى، تتوفّر طرق استخدام تدريجيّة كالتالي:

استخدام مكتبة C موجودة من Ada
كتابة الجزء المركزيّ من النظام بِـ Ada/SPARK فقط، مع إبقاء المحيط بِـ C/C++ كما هو
تحويل شيفرة Ada إلى DLL واستدعاؤها من لغات أخرى

ليست لغة «لا سبيل إليها سوى إعادة الكتابة الكاملة»، بل يمكن رفع الموثوقيّة تدريجيّاً من الأجزاء المهمّة، مع التعايش مع الأصول القائمة.

18. بيئة التطوير ── GNAT وAlire (تعمل حتّى على Windows)

قد تظنّ أنّ تجربة Ada تحتاج أدوات باهظة الثمن.

لكن اليوم، تتوفّر بيئة تطوير حقيقيّة مجّاناً.

GNAT        مترجم Ada المُضمَّن في GCC (مجّانيّ)
Alire       مدير حزم Ada وأداة بناء
GNAT Studio بيئة تطوير متكاملة (IDE) من إنتاج AdaCore
VS Code     يوفِّر امتداد Ada Language Server الإكمال التلقائيّ والقفز إلى التعريف

مع ظهور Alire خصوصاً (اسم الأمر هو alr)، أصبح الدخول إلى Ada أسهل بشكل جذريّ.

تجربة قريبة من cargo في Rust.

alr init --bin hello_ada
cd hello_ada
alr build
alr run

يُنشئ alr init المشروع، ويبنيه alr build، ويشغِّله alr run.

تحصل Alire أيضاً على سلسلة الأدوات (مترجم GNAT نفسه)، فلا حاجة حتّى لتثبيت المترجم يدويّاً.

يعمل على Windows وLinux وmacOS على حدٍّ سواء.

إن كنتَ تطوِّر على Windows، فالمسار الأقصر هو التالي:

1. احصل على مثبِّت Windows من الموقع الرسميّ لِـ Alire
2. أنشئ القالب عبر alr init --bin
3. أضف امتداد Ada (من إنتاج AdaCore) إلى VS Code
4. ابنِ الشيفرة عبر alr build وشغِّلها

يمكن أيضاً إضافة المكتبات عبر alr with اسم_المكتبة.

انتهى عصر «الفشل في إعداد البيئة».

19. نقاط ضعف Ada وتنبيهات

عرضنا سحر Ada حتّى الآن، لكن لِـ Ada نقاط ضعف أيضاً.

نرتّبها بإنصاف.

النظام البيئيّ صغير
  خيارات أقلّ في أطر عمل الويب وواجهات المستخدم الرسوميّة (GUI) وحزم SDK للسحابة وغيرها
  عدد حزم Alire يختلف بمرتبة عن اللغات السائدة

الكوادر والمعلومات قليلة
  المعلومات باللغة اليابانيّة قليلة بشكل خاصّ
  يجب احتساب تكلفة تدريب عند اعتمادها في تطوير جماعيّ

قد تُشعَر الصياغة بالإطالة
  إعلانات الأنواع والفصل بين المواصفة والجسم يبدوان ثقيلين للسكربتات الصغيرة
  لا تناسب استخدامات «التشغيل السريع فقط»

سوق التوظيف محدود
  منحصر في مجالات كالفضاء الجوّيّ والدفاع والسكك الحديديّة

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

حادث انفجار الصاروخ Ariane 5 في رحلته الأولى عام 1996، كان أحد أسبابه برمجيّات مكتوبة بِـ Ada.

نتيجةً لإعادة استخدام شيفرة كُتبت لِـ Ariane 4 في Ariane 5 ذي خصائص الطيران المختلفة، تسبَّبت قيمة كبيرة غير متوقَّعة أثناء التحويل في Constraint_Error، ولم تُعالَج بشكل مناسب فتوقّف النظام.

ما يُظهِره هذا الحادث هو التالي:

فحص وقت التشغيل في اللغة كشف المشكلة (لم ينكسر النظام بصمت)
لكنّ الافتراضات التشغيليّة تغيّرت دون أن تُتحقَّق
تصميم ما بعد حدوث الاستثناء (fail-safe) كان غير كافٍ

نظام الأنواع والعقود لا يُغنيان عن عمليّة إعادة النظر في الافتراضات.

اللغة جزء من هندسة الأمان، لا كلّها.

أظنّ أنّ هذا هو أصدق تنبيه عند تعلّم Ada.

20. البرمجيّات طويلة العمر وAda ── من منظور الصيانة

في موقعنا، نتعامل كثيراً مع صيانة وإطالة عمر الأصول القائمة على Windows.

من هذا المنظور، تحمل Ada سحراً آخر.

الأنظمة المكتوبة بِـ Ada لا يندر فيها أن تعمل مستمرّة لعقود.

وتصميم لغة Ada نفسه يفترض الصيانة طويلة الأمد.

الفصل بين المواصفة (.ads) والتنفيذ (.adb)
  -> يمكن لمن يصون النظام بعد 20 عاماً فهم الواجهة بقراءة المواصفة وحدها

الأنواع القويّة وقيود المدى
  -> تبقى الافتراضات الضمنيّة في الشيفرة نفسها، دون الاعتماد على النقل الشفهيّ أو التعليقات

العقد (Pre/Post)
  -> «وعد هذه الدالّة» يبقى بصيغة قابلة للتحقّق

فحص شموليّة case
  -> يُعدِّد المترجم مواضع التأثّر عند تغيير المواصفات

مراجعات المعيار تحترم التوافق مع الإصدارات السابقة
  -> كثير من شيفرة Ada 83 يعمل حتّى مع المترجمات الحديثة

يمكن استيراد هذه الأمور مباشرةً كمبادئ تصميم عند الصيانة طويلة الأمد بلغة C# أو C++ أيضاً.

عرِّف نوعاً ذا معنى (نوعاً يمثِّل معرِّفاً، أو نوعاً يحمل وحدة) بدلاً من int
صمِّم نوعاً لا يمكن إنشاء قيمة غير صحيحة منه (تحقّق في المُنشئ)
افصل الواجهة العامّة عن التنفيذ عمداً
عبِّر عن الشروط السابقة واللاحقة عبر التأكيدات (assertions) أو الاختبارات
اكتب switch للأنواع التعداديّة بشموليّة، وعامل التحذيرات كأخطاء

حتّى دون فرصة لاستخدام Ada في العمل، يستحقّ تعلّم فلسفة تصميمها قيمةً كافية.

كمادّة تعليميّة لتعلُّم حسّ «التعبير عن التصميم بالأنواع»، ما زالت Ada من الطراز الأوّل حتّى اليوم.

21. الخلاصة

استعرضنا سحر Ada.

نستعيد أهمّ النقاط.

Ada لغة حيّة استُخدمت في الأنظمة عالية الموثوقيّة لأكثر من 40 عاماً
اسمها مأخوذ من Ada Lovelace، والمعيار الأحدث هو Ada 2022
حتّى مع تطابق البنية، النوعان المختلفا الاسم نوعان مختلفان. الخلط بين الوحدات يتحوّل إلى خطأ في وقت الترجمة
بقيود المدى، يمكن منع القيم غير الصحيحة على مستوى النوع
تُفحَص حدود المصفوفات، فلا يتحوّل فيض المخزن المؤقّت إلى سلوك غير مُعرَّف
تفصل الحزمة بين المواصفة والتنفيذ، وتوضِّح أنماط المعطيات اتّجاه تدفّق البيانات
تُكتَب العمليّات المطلوبة في مواصفة الجينيريكس، فيصبح خطأ الاستخدام واضحاً
بعقد Ada 2012 (Pre/Post)، تبقى المواصفة داخل الشيفرة بصيغة قابلة للتحقّق
بالمهامّ والكائنات المحميّة، يمكن كتابة المعالجة المتوازية بأمان كميّزة لغويّة
باستخدام SPARK، يمكن ترقية العقد من فحص وقت التشغيل إلى برهان رياضيّ
بِـ GNAT وAlire، يمكن تجربتها فوراً ومجّاناً حتّى على Windows
صِغَر النظام البيئيّ وقلّة الكوادر نقطتا ضعف
آليّات الأمان في اللغة لا تُغني عن عمليّة إعادة النظر في الافتراضات

Ada لغة لم تصبح سائدة بمعنى الرواج.

لكنّ الأمان من نوع null، وفحص الشموليّة، والعقود، والصرامة القريبة من الملكيّة (ownership) ── وهي أمور تُدخِلها اللغات الحديثة بوصفها «ميّزات جديدة» ── كانت Ada تمتلك كثيراً منها منذ عقود.

يمكن تلخيص جوهر Ada بجملة واحدة:

الخطأ لا يُكتشَف، بل يجعله النوع والعقد «مستحيل الكتابة» أصلاً.

جرِّب في عطلة نهاية الأسبوع إنشاء مشروع واحد بِـ Alire، واكتب برنامجاً صغيراً بينما يوبِّخك المترجم.

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

المراجع

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

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

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

هل لغة Ada ما زالت مستخدَمة اليوم؟
نعم، ما زالت مستخدَمة. تُستخدَم منذ عقود في الأنظمة عالية الموثوقيّة حيث تتّصل الأخطاء مباشرةً بأرواح البشر أو بخسائر ضخمة، مثل نظم التحكّم في الطيران للطائرات المدنيّة، ومراقبة الحركة الجويّة، وأنظمة الإشارة والسلامة في السكك الحديديّة، والصواريخ والأقمار الصناعيّة، وأنظمة الدفاع، والأجهزة الطبّيّة. Ada لغة استمرّت مراجعتها لأكثر من 40 عاماً، بدأت بـ Ada 83 عام 1983، ومرّت بـ Ada 95 وAda 2005 وAda 2012، والمعيار الأحدث هو Ada 2022.
ما الفرق بين الأنواع القويّة في Ada واللغات الأخرى؟
في Ada، حتّى لو تطابقت البنية تماماً، فإنّ النوعين المُعلَنين باسمين مختلفين يُعامَلان كنوعين منفصلين تماماً. فمثلاً إذا أُنشئ نوعا Meters وSeconds كلاهما من Float، فإنّ خلطهما يسبِّب خطأ في وقت الترجمة، ويجب كتابة التحويل صراحةً. بالإضافة إلى ذلك، يمكن عبر subtype منح النوع قيداً على مدى القيمة (كأن يكون من 0 إلى 100)، وأيّ انتهاك لهذا القيد يتحوّل إلى استثناء Constraint_Error في وقت التشغيل. إنّه تصميم يمنع أخطاءً مثل الخلط بين الوحدات ليس عبر «الحذر»، بل عبر جعل «البناء نفسه لا يتمّ» من الأساس.
كيف يمكن تجربة لغة Ada مجّاناً؟
باستخدام GNAT (مترجم Ada المجّاني المُضمَّن في GCC) وAlire (مدير حزم Ada وأداة بناء، اسم الأمر alr)، تتوفّر بيئة تطوير كاملة مجّاناً على Windows وLinux وmacOS على حدٍّ سواء. يمكن الحصول على المثبِّت من الموقع الرسميّ لِـ Alire، ثمّ إنشاء القالب عبر alr init --bin، والبناء عبر alr build، والتشغيل عبر alr run، بتجربة قريبة من cargo في Rust. تحصل Alire أيضاً على سلسلة الأدوات نفسها، لذا لا حاجة لتثبيت المترجم يدويّاً. يتوفّر لِـ VS Code امتداد Ada من إنتاج AdaCore.
ما نقاط ضعف لغة Ada؟
من نقاط الضعف: صغر النظام البيئيّ (خيارات أقلّ من اللغات السائدة في أطر عمل الويب وواجهات المستخدم الرسوميّة (GUI) وحزم SDK للسحابة)، وقلّة الكوادر والمعلومات (خصوصاً باللغة اليابانيّة) ما يستلزم احتساب تكلفة تدريب، وشعور إعلانات الأنواع والفصل بين المواصفة والتنفيذ بالثقل في السكربتات الصغيرة، وانحصار سوق التوظيف في مجالات كالفضاء الجوّيّ والدفاع والسكك الحديديّة. كذلك، كما يُظهِر حادث Ariane 5 عام 1996، فإنّ آليّات الأمان في اللغة لا تُغني عن عمليّة إعادة النظر في الافتراضات.

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

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

غو كومورا

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

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

روابط عامة

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