بسم الله الرحمن الرحيم
هذا الدرس الثاني من تلخيص كتاب الساحر، الذي قد بدأناه وهذه مقدمته:
https://bassye.org/sicp_1_1/
يقول في الكتاب، الباب الأول، الفصل الأول (لبنات لغة البرمجة The Elements of Programming):
A powerful programming language is more than just a means for instructing a computer to perform tasks. The language also serves as a framework within which we organize our ideas about processes.
أي:
لغة البرمجة ليست عن أمر الحاسوب لتأديته المهمات المُرادة وحسب، بل تضع اللغة لنا أسلوب النظر في العملية.
وهنا أحسبُهُ والله أعلى وأعلم يشير إلى نماذج البرمجة، مثل نموذج البرمجة الوظيفي والكائني OOP، لأنه قال:
The language also serves as a framework within which we organize our ideas about processes.
ثم زاد وقال:
وعلى لغة البرمج تمكين المبرمج من دمج أفكار بسيطة مبتدعًا منها أخرى مركبة.
حتى تتصف اللغة بما قدمنا وتصير (بنت ناس وأفاضل) فلا مندوحة لها من ثلاث:
١) المكونات الأساسية،
٢) ووسائل ربط، تربط المكونات الأساسية معًا،
٣) ووسائل تجريد.
هذه اللبنات قوامُ بُنياننا المشيد (أقصد لغة البرمجة بالبنيان)، ودونها يتقوض البناء وينهد، ودونك بيان لكل واحدة:
المكونات الأساسية
نتعامل في البرمجة مع المعطيات data والإجراءات procedures (أو سمها الوظائف إن شئت)، والعلاقة بينهما أن الإجراء يُعالج المعطيات.
المعطيات قد تكون لائحة list من الأرقام تمررها إلى وظيفة procedure حتى ترتبها لك: عالجتِ الوظيفةُ المعطياتَ.
لِمَ أذكر هذا وحديثي عن المكونات الأساسية؟
لأخبرك أن المكونات الأساسية تنقسم قسمين:
١) معطيات Data،
٢) ووظائف Procedures.
المعطيات التي ذكرها كانت أرقامًا صحيحة integers مثل الواحد والاثنين والثلاثة، والأرقام العشرية 1.30، أما باقي المعطيات يقول لوقت لاحقٍ.
معطيات؟ كيف نعالجها؟
الوظائف تعالج المعطيات، والظائف المذكورة في الكتاب كانت الجمع والطرح والقسمة والضرب. كما قرأت، إنها وظائف!
التعبير
قبل أن نشرع في التطبيق، تبقى مفهوم واحد وثم نلج إلى المعمعة!
هذا المفهوم اسمه التعبير expression، أو سمه التعلمية instruction أو سمه الأمر command. ولتفهم هب أنك أمام الحاسوب وتكتب له تعبيرًا وهو يفهمه ثم ينفذه ويطبع لك ناتجه بعد تنفيذه (result of its evaluating).
هذا التعبير يجب أن نعبر عنه تعبيرًا يفهمه الحاسوب، نصيغه صياغة يفقهها الحاسوب. لو فتحت مفسِّر لغة Lisp وكتبتَ له العدد ٥٤٠ فإنه سيرجع القيمة نفسها.
أرجع القيمة؟
إذن فهمها، هي تعبير يفهمه الحاسوب، هذا التعبير يمثل عددًا، أدخلنا المعطى (وهو العدد) ثم أرجعه كما هو. فهمنا المعطيات، لكن والوظائف procedures؟
وسائل الربط
عرفنا التعبير الذي يمثل عددًا، والآن التعبير الذي يمثل وظيفةً أيضًا، وأول تعبير يمثل وظيفة هو الجمع.
نريد معالجة هذه المعطيات؛ نستعمل وظيفة لجمع رقمين معًا.
افتح DrRacket واكتب:
#lang scheme
(+ 200 300)
السطر الأول لنستعمل لغة scheme، أما السطر الثاني فتعبير محصور داخل أقواس، وهذا اسمه تركيبة Combination، إذ جمعنا أكثر من تعبير وحصرناه داخل الأقواس.
جمعنا تعابير داخل أقواس؟
إنها وسائل الربط (الأقواس)، وبهذا نجمع أكثر من تعبير معًا.
قلنا إن المعطيات مثل الأرقام = تعبير يمثل عددًا، وإشارة الزائد التي تراها = تعبير يمثل وظيفة. وهذه التعابير (التي تمثل عددًا والتي تمثل وظيفة) جمعناها معًا باستعمال الأقواس.
البادئة البولندية
كما ترى في التعبير المذكور:
#lang scheme
(+ 200 300)
إشارة الجمع في البداية، ثم تليها الأعداد التي نود جمعها، وهذا مغاير لما اعتدناه، ما اعتدناه كان جمع الأعداد بوضع إشارة الجمع بين رقمين، هكذا:
22 + 33
وإذا أردنا إضافة عدد جديد نضيف إشارة جمع جديدة:
22 + 33 + 44
الصيغة المستعملة في لغة Lisp اسمها صيغة البادئة البولندية prefix notation، الذي يبدأ بوضع الإشارة تليها الأعداد المراد جمعها.
والصيغ ثلاث:
أ. صيغة البادئة prefix notation،
ب. وصيغة التوسط أو التضمين infix notation، لأنها تضمن بين شيئين أو أكثر،
جـ. وصيغة اللاحقة postfix، التي تكون في الأخير:
(4 6 +)
إشارة الجمع نسميها العامل operator، وباقي الأرقام التي تليها نسميها بالمعمول operand. عامل لأنه يعمل في غيره، ومعمول {فيه} لأن العامل عمل فيه (أي المعمول).
وكما ترى في عامل الجمع + فإنه عمل في أكثر من معمول، متعدي إلى غيره، إلى أكثر من معمول.
من محاسن الصيغة البولندية:
١) تتعدى إلى أكثر من معمول، وقد يكون عددها كبيرًا، لكنها تبقى واضحة. انظر:
(+ 100 200 300 400 500 600 700 800 900)
كتبناها كلها باستعمال إشارة جمع واحدة، دون تشتت أو حيرة.
٢) التراكيب المتداخلة nested combinations، إذ تُدخِل أكثر من تركيب داخلها. مثلا تستعمل معاملًا آخر، الضرب مع الطرح:
(+ (* 5 6) (- 10 30))
انظر الأقواس الداخلية، القوس الأول الذي فيه الخمسة والستة، والنجمة إشارة الضرب، وثم انظر القوس الآخر الذي فيه إشارة الطرح.
الناتج سيكون 50.
ضع ما تشاء من التراكيب المتداخلة، لكنك قد تتوه لما تراها بذا المنظر:
(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))
ناتجها 57، ولكيلا تتوه وتستصعب الأمر سنعيد صياغتها بطريقة أخرى تكون بمنظر حسن:
(+ (* 3
(+ (* 2 4)
(+ 3 5)))
(+ (- 10 7)
6))
نسمي الطريقة بالطباعة الحسنة pretty-printing.
ومهما كانت التعبيرات معقدة ومركبة متداخلة فإن المفسر يتبع النهج نفسه في تطبيق العامل على المعمول: يقرأ التعبير المدخل، ينفذه، ويطبع ناتجه. ونسمي النهج المتبع باسم حلقة اقرأ-نفذ-اطبع REP loop.
وسائل التجريد
تعرفنا في الفقرات السابقة عن المكونات الأساسية ووسائل الربط، ونختم تلخيص اليوم بوسائل التجريد.
المكونات الأساسية ووسائل تمكن المبرمج من بناء وظيفة تؤدي مهمة، وتكون معقدة جدا، وحتى الآن كل عملنا على المعرفة التفصيلية: الخطوات اللازمة لتأدية عملية.
لكن أين المعرفة الإخبارية ولِمَ نحتاجها؟
التجريد لا مندوحة عنه، لا أحد سيحفظ كل سطر في البرنامج = نحتاج وسائل تجريد، المعرفة الإخبارية التي تخبرنا بماهية الشيء. مثلا تود برمجة وظيفة لحساب الجذر التربيعي لأي رقم، ولا تود كتابة الخطوات بالتفصيل كل مرة، بل تدخل القيمة للصندوق الأسود وهو يخرجها لك. فكيف نؤدي هذا؟
إسناد قيمة إلى اسم
يجيبك عنها الفصل المعنون بالتسمية والبيئة Naming and the Environment، وفحواه ربط قيمة باسم. مثلًا في الرياضيات شيء اسمه pi، قيمته ثابتة وهي (3.14159)، فقل لي بربك أتضيع مادة عمرك (وقتك) في تكرار كتابة الرقم كل مرة؟ أي شقاء سلطته على نفسك؟
الأسهل ربط القيمة باسم، وهذا ممكن في اللغة باستعمال كلمة define:
(define pi 3.14159)
(* pi 10)
أسندنا القيمة 3.14159 إلى الاسم pi ثم ضربناه بالرقم عشرة.
وترى أننا كتبنا كلمة تعريف define ثم pi (اسم الاسم المسند إليه القيمة) يليه الرقم عشرة (القيمة المسندة) دون كتابة قيمته. وهكذا نسند قيمة إلى اسم.
ما سبق اسمه إسناد قيمة value إلى متغير variable في البرمجة. وقد نسند ناتج عملية إلى متغير ولا إشكال:
(define result (* 20 30))
result
رويدك قف
هذه علبة تحوي غرضًا، فأين الوظائف procedure التي نمرر لها المعطى وهي تعالجه؟ تعلمناه اليوم!
إنشاء وظيفة procedure
الكاتب أرجأ شرحها في الكتاب ولم يشرحها في الدرس المعنون بالتسمية والبيئة، وكنت أنوي الإرجاء أيضًا لكن لا ضير من ذكرها الآن حتى تتلاحم الدروس ويصير بينها واشجة 🙂
صيغة إنشاء وظيفة procedure:
(define (<name_of_procedure> <formal_parameters>)
<the_body_of_procedure>)
نستعمل كلمة تعريف define المذكورة آنفًا، يليها أقواس وداخل الأقواس:
١. اسم الوظيفة name of procedure
٢ موسطات الوظيفة parameters (المعطيات التي سنمررها إلى الوظيفة)
اسم الوظيفة والموسطاتها تكون في أقواس، وهذا يميز التصريح عن وظيفة تعالج المعطيات من تصريح لإسناد قيمة إلى متغير.
٣. متن الوظيفة the body of procedure (متنها يحوي الخطوات بالتفصيل التي ستطبق على المعطيات الداخلة، إنها المعرفة التفصيلية، أما اسم الوظيفة فمعرفة إخبارية).
شرحنا الصيغة، وسنكتب وظيفة ترجع لنا تربيع الرقم المدخل. أول خطوة اسم الوظيفة وسيكون square، وبما أن الوظيفة تقبل رقمًا فإن المدخلات ليست إلا مدخلًا واحدًا، سنسمه number، أما متن الوظيفة (الخطوات المطبقة على المُدخَل): ضرب الرقم في نفسه:
(define (square number) (* number number))
اسم الوظيفة والموسط parameter داخل أقواس، لا تنسها.
سنستعملها الآن لنرى تربيع الرقم ٣:
(square 3)
وجب القول أنك تستطيع إنشاء وظيفة لا تقبل موسطات، بجعل اسم الوظيفة داخل الأقواس بلا أي موسطات، هكذا:
(define (without_parameters) (+ 5 5))
without_parameters
سيرجع المفسر قيمة، ويقول لك إن هذه وظيفة procedure، لأن اسم الوظيفة داخل أقواس أما إن لم تكن داخل أقواس فليست بوظيفة، إنما إسناد قيمة إلى متغير.
مثلا لو طبق ما يلي فإنه سيرجع لك ناتج جمع الخمستين:
(define (without_parameters) (+ 5 5))
without_parameters
ولا شك كلما زاد برنامجك تعقيدًا زادت الصناديق السوداء التي تحتاجها، وهذه الصناديق قد تكون وظائفًا أو علبة تحفظ قيمة، وهكذا تستوعب أجزاء البرنامج 🙂
وكل البرامج تتكون من وحدات صغيرة مجمعة، ومن محاسن التجزئة إلى أجزاء صغيرة أن تسرع وتيرة التطوير والاختبار، فلو كان في البرنامج علة bug في المكان الفلاني فتصليحها يسير.
وقبل الختام، أما فكرت في ربط قيمة باسم؟
لما تربط قيمة باسم فلا بد من تخزينها في مكان (ذاكرة)، ثم استخراجها عند الحاجة، وهذا المكان اسمه البيئة Environment، ودرسه في الفصل الثالث 🙂
بنية متكتلة block structure
هذه الفقرة تتحدث عن تعريف وظيفة داخل وظيفة. كل مرة نعرف الوظيفة منفصلة عن غيرها، لكنك تستطيع تعريف وظيفة داخل وظيفة ولا إشكال.
انظر هذا المثال وتدبره لوحدك، أصبحت الآن قادرًا على فهمه. عرفنا الوظيفة calculate، ثم داخلها عرفنا وظيفة أخرى اسمها add، ووظيفة أخرى اسمها subtract:
(define (calculate num)
(define (add x)
(+ num x))
(define (subtract x)
(- num x))
وتنبه: لما تُعَرِّف وظيفة داخل أخرى فأنت لن تقدر على استدعاء الوظيفة المُعَرَّفة داخل تلك الوظيفة، لأنها تكون خاصة بالوظيفة التي تحويها.
مثلا لو عندك الوظيفة أ، وعَرَّفت داخلها وظيفة أخرى اسمها ب، فلن تستطيع استعمال الوظيفة ب أبدًا، لأنها خاصة داخل الوظيفة أ فقط.
انتهى التلخيص أو الشرح، والسلام عليكم ورحمة الله وبركاته
الخاتمة
وها أنا أخط بقلمي الخطوط الأخيرة لهذا المقال الشائق!
وفي نهاية الأمر لا يسعني سوى أن أشكرك على حسن قراءتك لهذا المقال، وأني لبشر أصيب وأخطِئ، فإن وفِّقت في طرح الموضوع فمن اللّٰه عز وجل وإن أخفقت فمن نفسي والشيطان.
أرجو منك تقييم كفاءة المعلومات من أجل تزويدي بالملاحظات والنقد البناء في خانة التعليقات أو عبر حساب الموقع، والسلام عليكم ورحمة اللّٰه تعالى وبركاته.