مشكلة فونارج

في علوم الحاسوب ، تشير مشكلة وسيط الدالة (funarg problem) إلى صعوبة تنفيذ الدوال من الدرجة الأولى ( الدوال ككائنات من الدرجة الأولى ) في تطبيقات لغات البرمجة بحيث يتم استخدام تخصيص الذاكرة القائم على المكدس للدوال.

لا تظهر الصعوبة إلا إذا كان جسم دالة متداخلة يشير مباشرةً (أي ليس عبر تمرير الوسائط) إلى مُعرِّفات مُعرَّفة في بيئة تعريف الدالة، وليس في بيئة استدعاء الدالة. [ 1 ] يتمثل الحل القياسي إما في منع هذه الإشارات أو إنشاء دوال مغلقة . [ 2 ]

توجد نسختان مختلفتان قليلاً من مشكلة وسيط الدالة. تنشأ مشكلة وسيط الدالة الصاعدة من إرجاع (أو تمرير "صاعدًا") دالة من استدعاء دالة. أما مشكلة وسيط الدالة الهابطة فتنشأ من تمرير دالة كمعامل إلى استدعاء دالة أخرى.

مشكلة التحويل التصاعدي

عندما تستدعي دالة A دالة أخرى B أثناء تنفيذ برنامج نموذجي، يجب تخزين الحالة المحلية لـ B (بما في ذلك المعاملات والمتغيرات المحلية ) في مكان ما. في دالة طرفية ، يمكن تخزين هذه الحالة في سجلات، ولكن إذا استدعت B دالة أخرى C تستخدم هذه السجلات، فيجب حفظها في مكان ما في الذاكرة حتى تتمكن B من المتابعة بعد انتهاء C.

في معظم البرامج المُترجمة، تُخزَّن هذه الحالة المحلية في مكدس الاستدعاءات ضمن بنية بيانات تُسمى إطار المكدس أو سجل التنشيط . يُضاف إطار المكدس هذا، أو يُخصَّص، قبل أن تستدعي الدالة B الدالة C، ويُزال منه، أو يُحرَّر، قبل أن تُعيد B قيمة. تظهر مشكلة الوسيط التصاعدي عندما تكون القيمة المُعادة من B دالة F تُشير إلى حالة B (مثلاً، متغير محلي V) بعد أن تُعيد B قيمة. قد تستدعي الدالة A الدالة F، التي تحتاج إلى V مُخصَّصًا. لذلك، يجب عدم تحرير إطار المكدس الذي يحتوي على متغيرات حالة B عند عودة B، مما يُخالف نموذج استدعاء الدوال القائم على المكدس .

أحد حلول مشكلة الوسائط التصاعدية هو ببساطة تخصيص جميع سجلات التنشيط من الذاكرة الديناميكية (heap) بدلاً من الذاكرة المكدسة (stack)، والاعتماد على نوع من أنواع جمع البيانات المهملة أو عدّ المراجع لتحريرها عند انتهاء الحاجة إليها. تاريخيًا، كان يُنظر إلى إدارة سجلات التنشيط في الذاكرة الديناميكية على أنها أقل كفاءة من إدارتها في الذاكرة المكدسة (على الرغم من وجود بعض التناقضات في هذا الرأي [ 3 ] )، كما كان يُعتقد أنها تُضيف تعقيدًا كبيرًا إلى عملية التنفيذ. لا تُنشئ معظم الدوال في البرامج التقليدية (وخاصةً في البرامج المكتوبة بلغات البرمجة الوظيفية ) وسائط تصاعدية، مما يزيد من المخاوف بشأن التكاليف الإضافية المحتملة المرتبطة بتنفيذها. علاوة على ذلك، يُعدّ هذا النهج صعبًا للغاية في اللغات التي لا تدعم جمع البيانات المهملة.

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

يتمثل حل آخر في نسخ قيمة المتغيرات إلى الدالة المغلقة عند إنشائها. سيؤدي هذا إلى سلوك مختلف في حالة المتغيرات القابلة للتغيير، لأن الحالة لن تكون مشتركة بين الدوال المغلقة. ولكن إذا كان معروفًا أن المتغيرات ثابتة، فسيكون هذا الأسلوب مكافئًا. تتبع لغات ML هذا النهج، لأن المتغيرات في تلك اللغات مرتبطة بقيم - أي لا يمكن تغييرها. كما تتبع جافا هذا النهج فيما يتعلق بالفئات المجهولة (والتعبيرات اللامدا منذ جافا 8)، حيث تسمح فقط بالإشارة إلى المتغيرات في النطاق المحيط التي تكون finalثابتة فعليًا.

تتيح بعض لغات البرمجة للمبرمج اختيار أحد السلوكين صراحةً. تتطلب الدوال المجهولة في PHP 5.3 تحديد المتغيرات المراد تضمينها في الدالة المغلقة باستخدام use ()عبارة `const`؛ فإذا تم سرد المتغير بالمرجع، فإنه يتضمن مرجعًا إلى المتغير الأصلي؛ وإلا، فإنه يمرر القيمة. أما في دوال Apple Blocks المجهولة، فإن المتغيرات المحلية المُلتقطة تُلتقط افتراضيًا بالقيمة؛ وإذا أراد المبرمج مشاركة الحالة بين الدوال المغلقة أو بين الدالة المغلقة والنطاق الخارجي، فيجب تعريف المتغير باستخدام __blockالمُعدِّل `const`، وفي هذه الحالة يُخصص هذا المتغير في الذاكرة الديناميكية (heap).

مثال

يُعرّف الكود الزائف التالي ، الشبيه بلغة هاسكل، تركيب الدوال :

compose f g = λx f ( g x )

λهي المعامل المستخدم لإنشاء دالة جديدة، والتي في هذه الحالة تأخذ وسيطًا واحدًا، x، وتعيد نتيجة تطبيقها أولًا gعلى x، ثم تطبيقها fعلى ذلك الوسيط. تحمل هذه الدالة λ الدالتين fو g(أو مؤشرات إليهما) كحالة داخلية.

تكمن المشكلة في هذه الحالة إذا قامت دالة التركيب بتخصيص متغيرات المعاملات fعلى gالمكدس. عند composeانتهاء الدالة، يتم تجاهل إطار المكدس الذي يحتوي على هذه المتغيرات f. gوعندما تحاول الدالة الداخلية λxالوصول إلى هذه gالمتغيرات، فإنها ستصل إلى منطقة ذاكرة مهملة.

مشكلة الدوال الهابطة

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

يجب أن تكون معاملات الدوال الهابطة متوافقة مع تنفيذ معاملات الدوال الصاعدة. على سبيل المثال، لنفترض أن الدالة A تستدعي الدالة B، التي تحسب قيمة الدالة F بالإشارة إلى متغير محلي V في B. ثم تمرر B قيمة F كمعامل إلى الدالة C، التي تستدعي F (معامل دالة هابطة) قبل أن تُعيد القيمة. بعد ذلك، تستخدم B المتغير المحلي V، وأخيرًا تُعيد F إلى A، التي تستدعي F أيضًا (معامل دالة صاعدة). يجب أن تشترك B وكلا استدعاءي F في نفس المتغير المحلي V.

تُعقّد مشكلة الدوال الهابطة عملية الترجمة الفعّالة لاستدعاءات الذيل . عادةً، إذا كان آخر إجراء تقوم به الدالة B قبل العودة إلى الدالة A هو استدعاء الدالة C (وإعادة قيمة C المُعادة إلى A دون تعديل)، فيمكن للدالة B تحرير إطار مكدسها قبل استدعاء C، تاركةً المكدس كما لو أن A استدعت C مباشرةً. تضمن بعض لغات البرمجة إمكانية تنفيذ استدعاءات الذيل المتكررة عددًا غير محدود من المرات في مساحة مكدس ثابتة ومحدودة. مع ذلك، إذا مررت الدالة B الدالة F إلى C، وكانت F تشير إلى متغير محلي V في B، فيجب الحفاظ على إطار مكدس B (أو على الأقل المتغير V) حتى تعود C.

تحدث المشكلة نفسها مع الشيفرة المكتوبة بأسلوب تمرير الاستمرارية ، حيث لا تُعيد الدوال قيمة، بل تُنهي عملها بتمرير قيمها المُعادة إلى دوال استمرارية (لا تُعيد قيمة). يجب تنفيذ هذه الاستدعاءات كاستدعاءات ذيلية لمنع تسرب غير محدود للذاكرة . هذا يُحوّل حالة دالة ذات وسيط تصاعدي (حيث تُعيد الدالة B القيمة F) إلى حالة دالة ذات وسيط تنازلي (حيث تُمرّر الدالة B القيمة F إلى دالة الاستمرارية)، ومرة ​​أخرى، يجب الحفاظ على إطار مكدس الدالة B طالما كان من الممكن استدعاء الدالة F.

الآثار العملية

تاريخيًا، أثبتت مشكلة تمرير الدوال تصاعديًا أنها أكثر تعقيدًا. على سبيل المثال، تسمح لغة البرمجة باسكال بتمرير الدوال كوسائط، ولكن لا تسمح بإرجاعها كنتائج؛ لذا، تتطلب تطبيقات باسكال معالجة مشكلة تمرير الدوال تنازليًا، وليس تصاعديًا. تسمح لغتا البرمجة موديولا-2 وأوبرون (المشتقتان من باسكال) بتمرير الدوال كوسائط وقيم مُرجعة، ولكن لا يجوز أن تكون الدالة المُسندة دالة متداخلة. تاريخيًا ، تتجنب لغة البرمجة سي الصعوبة الرئيسية لمشكلة تمرير الدوال بعدم السماح بتداخل تعريفات الدوال؛ نظرًا لأن بيئة كل دالة متطابقة، وتحتوي فقط على المتغيرات والدوال العامة المُخصصة بشكل ثابت، فإن المؤشر إلى كود الدالة يصفها بشكل كامل. اقترحت شركة آبل ونفذت صيغة إغلاق للغة سي تحل مشكلة تمرير الدوال تصاعديًا عن طريق نقل الإغلاقات ديناميكيًا من المكدس إلى الكومة حسب الحاجة. تتعامل لغة البرمجة جافا مع هذا الأمر من خلال اشتراط تعريف السياق المستخدم بواسطة الدوال المتداخلة في الفئات الداخلية والمحلية المجهولة ، وأن يكون السياق المستخدم بواسطة تعابير لامدا نهائيًا فعليًا. أما لغتا C# و D فتستخدمان تعابير لامدا (إغلاقات) تُغلّف مؤشر الدالة والمتغيرات المرتبطة به.final

في اللغات الوظيفية ، تُعتبر الدوال قيمًا من الدرجة الأولى يمكن تمريرها إلى أي مكان. لذا، يجب على تطبيقات Scheme أو Standard ML معالجة مشكلتي تمرير الدوال تصاعديًا وتنازليًا. ويتحقق ذلك عادةً بتمثيل قيم الدوال كدوال مغلقة مُخصصة في الذاكرة الديناميكية ، كما سبق شرحه. يستخدم مُترجم OCaml تقنية هجينة (تعتمد على تحليل البرنامج الثابت ) لزيادة الكفاءة إلى أقصى حد.

انظر أيضاً

مراجع

  1. وظيفة الدالة في لغة ليسب أو لماذا يجب تسمية مشكلة FUNARG بمشكلة البيئة ، بقلم جويل موسى، مذكرة مشروع MAC في معهد ماساتشوستس للتكنولوجيا AI-199، MAC-M-428، يونيو 1970 (15 صفحة).
  2. حل مقترح لمشكلة FUNARG ، بقلم إريك ساندوال، في: نشرة ACM SIGSAM 17 (يناير 1971)، ص 29-42.
  3. أبيل، أندرو دبليو ؛ شاو، تشونغ (مارس 1994). دراسة تجريبية وتحليلية لتكلفة المكدس مقابل تكلفة الكومة للغات ذات الإغلاقات (ملف PDF) (تقرير فني). جامعة برينستون. برينستون CS TR-450-94 .