الأحد، 10 فبراير، 2013

استبدال الإجراءات متعددة الخرج بالإجراءات الداخلية

هناك لغات برمجةٍ تسمح بتفريع الإجراءات functions nesting، أي أنها تسمح بتعريف إجراءٍ أو أكثر داخل إجراءٍ آخر، و من تلك اللغات لغة الـpython علي سبيل المثال. و في أمثال تِلكم اللغات نري أننا نحصل علي فائدةٍ جميلةٍ من تفريع الإجراءات، حيث أن الإجراءات الداخلية يمكن أن تكون قادرةً علي التعامل مع المتغيرات و الثوابت و ما يشبههن اللاتي تم تعريفهن في الإجراءات الخارجية قبل تعريف الإجراء الداخلي، و كذلك التغيير في قِيم تلكم العناصر بحرية.
كما في المثال التالي المكتوب بالـpython:
def  func1():
    x = [10]
    y = [20]
   
    def  func2():
         x[0] = x[0]+ 1
         y[0] = y[0] + 1
   
    func2()
   
    print x[0],"    ", y[0]
   
func1()

الذي يعطي عند تنفيذه الخرج التالي:
11         21

و لو حاولنا إعادة صياغة البرنامج السابق بلغة لا تدعم تفريع الإجراءات مثل الـjava لوجدنا أننا سنضطر لكتابة كودٍ يشبه ما يلي:

public class Test {
    public static void main(String[] args) {
        Test t = new Test();
        t.func1();
    }
   
    void func1() {
        int x = 10, y = 20;
        result res = func2(new result(x, y));
        System.out.println("" + res.x + "    " + res.y);
    }

    result  func2(result res){
        res.x = res.x +1;
        res.y = res.y +1;
        return res;
    }

    class result {
        int x, y;
        public result(int x, int y){
            this.x = x;
            this.y = y;
        }
    }
}
و نري فيه أننا اضطررنا إلي عمل صنفٍ جديدٍ result يحوي داخله البيانات المتعددة التي نرغب في إعادتها لمحاكاة عمل الإجراءات الداخلية. و الأمر في المثال السابق يتعلق بمحاكاة عمل إجراءٍ داخليٍ واحد، فكيف يكون الحال مع محاكاة عمل أكثر من إجراءٍ تتعامل مع أكثر من من حاويةٍ داخلية ؟!
بالطبع ستكون هذه من الأمور المرهقة للمبرمج، و كلما ازدادت المشاريع البرمجية ضخامةً كلما قابل المبرمج مواقفاً تشبه السابقة و لكنها أكثر تعقيداً.
لكن هذه المشكلة يمكن حلها بمنتهي البساطة إذا كانت الإجراءات قادرةً علي إعادة أكثر من خرج، كما هي الحالة مع لغة إبداع، و بالنظر إلي بناء المثال السابق بإبداع:


إجراء1()


إجراء  إجراء1 :
    رقم س = 10
    رقم ص = 20
   
    س ، ص = إجراء2(س    ص)
   
    أكتب.نص.سطر(إلي.نص(س) +"       " + إلي.نص(ص))
   
إجراء  (رقم س2  رقم ص2)  إجراء2(رقم س1  رقم ص1):
    س2 = س1 +1
    ص2 = ص1 +1

   
   
و له تقريباً نفس عدد أوامر البرنامج المكتوب بالـpython، بل و يمكن اختصار البرنامج السابق أكثر من ذلك بجعل كود الإجراء إجراء2 كما يلي:

إجراء  (رقم س2=س1+1 رقم ص2=ص1+1)  إجراء2(رقم س1  رقم ص1):
    لاشيء

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

كما أنه يمكن الاستغناء عن الإجراءات الداخلية عن طريق التمرير بالمرجع passing by reference، و هو الأمر الموجود في لغاتٍ كثيرةٍ منها الـC  و الـ++C و الـ object pascal و الـD و غيرهن من اللغات الأخري. و يمكننا كتابة المثال السابق بلغة object pascal كما يلي*:

function func2(var x, y :integer);
begin
    x = x+1
    y = y+1
end;

function func1();
begin
    x :integer = 10;
    y :integer = 20;
   
    func2(x,y);
   
    writeln(x +"    " +y);
end;

begin
    func1();
End.

و بالطبع يمكنكم ملاحظة أنه تم الاستغناء عن الإجراءات الداخلية هنا أيضاً بكفاءة، لكن أيضاً يمكنكم ملاحظة أن تعقيد قواعد الـobject pascal جعل الأمر أقل سهولةً من حالتَيْ الـpython و إبداع.

كما أن هناك نقطةً أخري تتعلق بإمكانية حدوث ارتباكاتٍ و مشاكلٍ منطقيةٍ في اللغات التي تدعم الإجراءات الداخلية، حينما تكون تلك اللغات من نوع اللغات استنتاجية الأنواع
Type inference، فمثلاً لو نظرنا إلي البرنامج الأول المكتوب بلغة الـpython لوجدنا أنه يستخدم الـlists و ليس المتغيرات الرقمية البسيطة، فلماذا لم أكتبه كما يلي:
def  func1():
    x = 10
    y = 20
   
    def  func2():
         x = x + 1
         y = y + 1
   
    func2()
   
    print x,"    ", y
   
func1()

علي الرغم من أن هذا هو الأمر الأقرب للمنطق ؟!
الجواب أنني حينما فعلتُ ذلك أَخرجَ لي مُفسِّر الـpython الخطأ التالي:
x = x + 1                                                                                                            
UnboundLocalError: local variable 'x' referenced before assignment
و السبب في هذا أنه اعتبر أن المتغيرين x و y داخل الإجراء func2 يختلفان عن المتغيرين x و y اللذين في func1 ! و بالتالي أصبح من الخطأ كتابة الأمرين:
x = x + 1
y = y + 1
لأننا بهذا نكون قد استخدمنا المتغيرين x و y قبل إعطائهما أي قيمة، رغم أن المبرمج يقصد أن يستخدم x و y الخاصين بالإجراء func1 و اللذان تم إعطاؤهما قيماً ابتدائيةً هن 10 و 20 علي الترتيب !

و الغريب أنك لو عرَّفتَ list داخل func1 فيمكنك استخدام عناصرها الداخلية بشكلٍ عاديٍ في func2، و هو ما لجأتُ إليه فعلاً في المثال الأول. و لكنك لو أسندتَ قيمةً لاسم تلك الـlist في الإجراء الداخلي
func2 فإنه سيعتبر أنك تقوم بتعريف list جديدة داخل func2 تُغطِّي تلك التي تحمل نفس الاسم في func1 !
أي أنهً عند تنفيذ المثال التالي:

def  func1():
    x = [10]
    y = [20]
   
    def  func2():
         x = [11]
         y = [21]
   
    func2()
   
    print x[0],"  ",  y[0]
   
func1()
تكون النتيجة:
10    20

لأن القِيم [11] و [21] أُسنِدت إلي x و y  الخاصَّيْن بـfunc2، بينما بقي x و y الخاصَّيْن بـfunc1 كما هما، و هذا يعني معضلةً عند محاولة المبرمج التعديل في قيمة x أو y  في func1 من داخل func2؛ حيث أنني حينما حاولتُ إيجاد طريقةٍ لفعل ذلك تختلف عن استخدام خاصية الخرج المتعدد: لم أجد، و هو ما يؤيد عدم الحاجة الضرورية لتفريع الإجراءات، و أن كل الفوائد التي يمكن جنيها من ورائه يمكن الحصول عليها بشكلٍ أبسط و أسهل عن طريق الإجراءات متعددة الخرج. 
كما أن هذا الأمر يؤكد أيضاً ما ذهبتُ إليه من أفضلية فرض التعريف اليدوي للأنواع علي المبرمج و عدم جعل اللغة من النوع الذي يستنتج الأنواع بدون تدخل المبرمج**.


----------------------------------
* كل البرامج المكتوبة في هذا المقال تم تنفيذها عملياً و التأكد من خلوها من الأخطاء، ما عدا البرنامج المكتوب بالـobject pascal.
** تجدون كلامي عن هذه الجزئية في فصل (فائدة تعريف الحاويات يدوياً) الموجود في باب (أين الإبداع فيها؟) في نهاية كتاب (رسالة البرمجة بإبداع).

ليست هناك تعليقات:

إرسال تعليق