برمجية whiptail لبرمجة برامج واجهة مستخدم مبنية على النص TUI بالباش “bash”

في رحلتك لتعلم النظام واستعماله، قد مررت على الأرجح ذات مرة بتثبيت نظامٍ عبر المِطراف، أو استعمال أداة تعرض لك مربعات وقوائم، كل ما يتطلب منك إدخال مدخل نصي، أو التحريك بالاسهم نزولًا وصعودًا والضغط على زر مسافة space لتحديد شيءٍ ما، كهذه الصورة:

أو كهذه:

وتساءلتَ كيف برمجها؟ وماذا تُسمَّى؟ وهل مِن المُمكِن برمجتها بنفسك؟
نعم، مِن المُمكِن برمجة برامج مشابهة طبق الأصل، فهذه ما تسمَّى بواجهةِ مستخدمٍ مبنيةٍ على النَصّ text-based user interface تُختصر بثلاثةِ حروفٍ TUI، وموضوعنا اليوم حول استعمال برمجيَّة whiptail التي تُمكِنُنا مِن برمجة برامج واجهة مستخدم مبنية على النَصّ بيسر وسهولة.
ما whiptail
هي برمجيَّة تُمكّن المُبَرمِج مِن عرضِ مربَّعات حوار dialog boxes في المِطراف Terminal مِن داخلِ نصوصٍ برمجيَّة بلغة باش bash في لينُكس لأيَّ غرض يُرِيده المُبَرمِج، وتُعدُّ صديقة للمستخدمِ لسهولة فهمها، ولسهولتها الجذابة المُغرية، ومريحَة للمُبَرمِج أيضًا إذ تجعَله يتنبَّأ بمدخلاتِ المستخدم بسطورٍ قليلة وبطريقةِ عرض جميلة.
هذه ليست البرمجيَّة الوحيدة، البرامج الشبيهة بها كثيرة، وعندنا برمجيات أخرى تخصُّ واجهة المستخدم الرسومية GUI، تَعرض مربَّعات حوار رسومية من داخل نصوص برمجية بلغة باش، مثل xdialog لعرض نوافذ X11، وzenity لعرض نوافذ GTK (يعرف zenity باسم gdialog).
إما البرمجيات الأخرى التي تعمل عمل whiptail فلدينا dialog، الذي يُعدُّ نسخة محسَّنة منه، ولديه خيارات متنوعة، ومقال اليوم تستطيع تطبيقه على dialog أيضًا بنفس الخيارات.
المتطلبات
قبل البدء في استعماله يجب أن تكون عارِفًا بلغة باش، وأن تثبَّت برمجيَّة whiptail بمدير الحُزم الخاصّ بتوزيعك، فإن لَمْ تجده فثبَّت dialog، وأنشِئ اسمًا مستعارًا له باسمِ whiptail، وتابِع المقال:
alias whiptail="dialog"
مربع المعلومة info box
قد تودُّ عرض رسالة تُخبِر المستخدم بفعل شيءٍ وثُمَّ الخروج من البرنامج، مثلًا أخباره بأن يُشغِّل الملف بمستخدم الجذر root، ولهذا الغرض سنستعمل مربَّعات الحوار في whiptail، وتحديدًا مربَّع المعلومة info box.
يَعرضُ هذا المربَّع معلومة للمستخدمِ، مربَّع به نصٌّ ما. صياغة الأمر:
whiptail --infobox text height width
شرح صيغة الأمر:
whiptail
البرنامج المسؤول عن عرضِ مربَّع الحوارات.
–infobox
الخيار الذي نُرِيدهُ مِن البرنامج، وهو الخيار الذي يمكننا من استعمال مربع المعلومة.
text
نصُّ الرسالة التي ستظهر في مربع المعلومة. إن كانت أكثر مِن كَلِمة فاكتبها بين علامتي تنصيص.
مثلًا نُرِيدُ طِباعة رسالة فحواها: “شغلني بمستخدم الجذر رجاءً”، إذا شغَّله بمستخدم عادي.
height
إرتفاع مُربَّعنا، فالمربَّع الذي نُنشِئهُ نُحدِّدُ طوله نحن.
width
عرض مُربَّعنا، حالهُ كحالِ الطول، نُحدِّدهُ أيضًا.
بَعدَ فِهم الأمر سنكتبُ إخطاطة script بلغة bash لتطبيقِ ما تعلمناه (استعمل أيَّ محرر نصوص يُعجبك) بالمحتويات الآتية:
#!/bin/bash
if ! [[ $(id -u) -eq 0 ]]; then
whiptail --infobox "Run me as root" 10 97
exit 0
fi
النَصُّ السابق واضح جدًا، عِبارة شَرطيَّة تتحققُ من المستخدم الذي شغَّل الملف هل هو مستخدم الجذر root أم لا؟
بعضهم قد لا يعمل لديه وسيرى خلل فيه، وهذا خلل بِمُربَّعِ المعلومة لأنه لا يعملُ في بعض المطاريف Terminals (جمع مِطراف Terminal)، لذا تحتاج إلى إضافة TERM=ansi قَبْل whiptail، فيكون الأمر:
TERM=ansi whiptail --infobox "Run me as root" 10 97

يفضل استعمال مربَّع المعلومة عند عرض شيء للمستخدم ثُمَّ الخروج.
إضافة عنوان لأي مربع حوار
هل ترى المربَّع الذي ظَهَر؟ هل تودُّ إضافة عنوان في أعلاه؟
لإضافة عنوان نستعمل الخيار title–، الذي يعني عنوان، لنخبره بأننا نُرِيدُ إضَافة عنوان، ثُمَّ نتبعُه بنصِّ العنوان المراد عرضه:
whiptail --title "INFOBOX" --infobox "This is a INFOBOX" 10 97

تذكَّر بأن لكل مربَّع حوار عنوان في أعلاه، فخيار إضَافة العنوان تستطيع استعماله مع بقيّة الأمثلة التي سترد بعد قليل، فلن أعيد شرحه مرة أخرى.
مربع الرسالة message box
مربَّع الرِسالة يُشبِه إلى حدٍ كبير مربَّع المعلومة، باختلافٍ بينهُما، وهو أن مربَّع الرِسالة message box ينتظرُ المستخدمُ ليضغط زر الموافقة (كما سنراه بعد قليل)، بينما مربَّع المعلومة لا يَنتظرُ المستخدم ليضغط أي شيء، فإنه سيظهر ويختفي أو يتوقف البرنامج.
صيغته:
whiptail --msgbox text height weight
الأمور سهلة الآن، فلقد عرفنا النمط المستخدم في مربَّعات الحوار، النص text: النص المراد عرضه، الارتفاع height: ارتفاع المربَّع، والعرض widght: عرض المربَّع، وكليهُما يقبلان عددان صحيحان.
لنفترض بأنَّ لدينا إخطاطة باش bash script تُحدِّثُ النِظام، ولنفترض أنه صدر إصدار نواة جديد، والمستخدم وصل mount بعض الأجهزة أو ما زال يعمل على شيء هام، فأنت لا تريد أن تُطفئ النِظام فجأة لتحميل النواة الجديدة.
فهنا يأتي مربَّع الرِسالة ليحل لك المشكلة:
#!/bin/bash
if ! [[ $(id -u) -eq 0 ]]; then
whiptail --infobox "Run me as root" 10 97
exit 0
fi
whiptail --title "message box" --msgbox "When you press enter I will reboot your system" 10 97

تغيير كلمة ok في الزر
لم تروق لك كلمة ok في الزر وتريد تغييرها إلى done مثلًا؟
تستطيع تغييرها بالخيار ok-button– متبوعًا بالنصِّ المرادِ تغييره إليه:
whiptail --title "Message box" --ok-button "Done" --msgbox "Hello World!" 8 97
تذكَّر أن أيّ مربَّع حوار يمتلك زرار موافقة ok button تستطيع تغييره بالخيار السابق.
مربع نعم ولا Yes&No box
كما يوحي اسمه، مربَّع الحوار نعم ولا Yes&No box، يُعطِيك القُدرة لاختِيار فعل شيء أو رفضه.
لنعود إلى مثالنا السابق الذي يُحدَّثُ النِظام وثُمَّ يُعيدُ تَشغيله. ماذا لو سألنا المُستخدم عن رأيه هل يريد إعادة التشغيل بدل إعادة تشغيله بغتَةً؟
في هذه الحالة سنستعمل مربَّع الحوار نعم ولا، وصيغته:
whiptail --yesno text height weight
لا داعي لشرحه صحيح؟
النمط معروف وسهل الملاحظة. نعود لمثالنا السابق، سنسأل المستخدم سؤالًا سهلًا، وسنخبره بكل لباقة واحترام، هل تفضل يا سيدي الموقَّر إعادة تشغيل الجهاز أم لا؟
whiptail --title "YES/NO BOX" --yesno "Did you shutdown the computer?" 8 97

الأمر السابق يُعيد رمز حالة الخروج exit status code، والذي سيكون صفرًا إن ضغط المستخدم على زر القبول yes، ولو ضغط على زر الرفض سيرجع القيمة 1، العدد 1، وأما لو ضغط المستخدم على زر ESC للخروج من البرنامج فإنه سيرجع القيمة 255.
وبمعرفتك هذه المعلومة تستطيع إدخال العبارات الشرطيَّة. لا تقلق سترى مثالًا بعد قليل.
الآن انظر في الصورة التي في الأعلى، المؤشر cursor على الزر نعم، إن أردت أن يكون المؤشر على الزر لا، فالخيار defaultno– يفعلُ ذلِكَ:
whiptail --title "YES/NO BOX" --defaultno --yesno "Did you shutdown the computer?" 8 97

تغيير نصي yes وno
إن أردت تبديل كَلِمة yes بشيء آخر، مثل: “نعم، أريد إطفاء الحاسوب”، وكَلِمة no بشيءٍ آخر مثل: “لا، لا أريد إطفاءه” فسنستعمل الخيار yes-button– متبوعًا بالنصِّ الجديد، وبنفس الخطوة أيضًا مع no، نستخدم الخيار no-button– متبوعًا بالنص الجديد:
whiptail --title "YES/NO BOX" --yes-button "Yes, I am a member" --no-button "No, I am not a member" --yesno "Are you a member of the aosus community?" 8 97

مربع الإدخال input box
مربَّع الإدخال، يطلبُ مِن المُستخدم إدخال قيمة ما، صيغته:
whiptail --inputbox text height width [init]
القيمة الأخيرة اختيارية، لا ضير من تركها فارغة، عند وضع نصّ مكان تلك القيمة فإن مربع الإدخال حين يظهر ستكون تلك القيمة في مربع الإدخال افتراضيًا.
عند تثبيتي لتوزيعة VoidLinux وكتابة اسم المستخدم، ظهر مربَّع الإدخال بقيمةٍ افتراضيَّةٍ كانت void، فتستطيع تغييرها أو الضغط على زر الموافقة لاختياره وإكمال التثبيت.
مثال لمربَّع الإدخال:
whiptail --title "username" --inputbox "enter your username: " 7 87 MrNarsus

تغيير زر الإلغاء
تعلمنا سابقًا تغيير نصّ زر الموافقة باستعمال ok-button–، ولتغيير زر الإلغاء cancel button نستعمل الخيار cancel-button– متبوعًا بالنصِّ الجديد:
whiptail --title "username of system" --cancel-button "Back" --inputbox "Enter Your username: " 7 87 Mr-Narsus

وجب التنبيه بشأن مربَّع الإدخال، إلى عِلّة فيه، فهو لا يُرسِل المدخل إلى الخرج القياسي stdout بل يطبعه إلى خرج الخطأ القياسي stderr، وهذا لن يسمح لك بتخزين القيمة في مُتغيَّر variable.
لحل هذه المشكلة سنعكس العملية وسنجلعه يُعيدُ توجيه الخطأ القياسي إلى الخرج القياسي:
3>&1 1>&2 2>&3
وشرح الصيغة السابقة:
1. أنشأنا واصف ملف file descriptor ثالث يُشير إلى الدخل القياسي، الذي يمتلك واصف الملف 1.
2. أعدنا توجيه الخرج القياسي إلى خرج الخطأ القياسي الذي يمتلك واصف الملف 2.
3. أعدنا توجيه خرج الخطأ القياسي إلى واصف الملف 3، الذي يُشِير بذاته إلى الدخل القياسي.
الآن سنجرب تخزين مدخل إلى مُتغيَّرٍ ثُمَّ طبعه:
#!/bin/bash
name=$(whiptail --title "Your Name" --ok-button "Done" --cancel-button "Exit" --inputbox "Enter Your Name: " 8 85 3>&1 1>&2 2>&3)
whiptail --title "Welcome message" --ok-button "Exit" --msgbox "Hello Sir $name" 10 83


إخفاء زر الإلغاء
لإخفاء زر الإلغاء نستعمل الخيار nocancel–:
whiptail --title "Your Name" --ok-button "Done" --nocancel --inputbox "Enter Your Name: " 8 85

إن أردت تكبير الأزرار فاستخدم اللاحقة fb– التي تجعل الزر يظهر بمظهر أكبر، بإضافتها إلى الأمر كأنها خيار عادي.
مربع النص text box
يَسمحُ لك بعرض محتويات ملف ما، كأنه مستعرض ملفات لكِنه بدائيّ جدًا. صيغته:
--textbox filename height weight
مثال:
whiptail --title "simple files viewer" --ok-button "Back" --textbox filename.txt 8 82

عند عرض محتويات ملف ما، والملف به سطور كثيرة فلن تظهر كُلَّها، ولن تستطيع النزول لأسفل أو الصعود لأعلى، لذا تحتاج إلى اللاحقة scrolltext–:
whiptail --title "simple files viewer" --ok-button "Back" --scrolltext --textbox filename.txt 8 82

مربع كلمة السر password box
يَسمحُ لك باستقبال كلمة سرٍ مِن المستخدمِ، عِندما يكتُبها لن تظهر له، ما ستظهر له نجمات. صيغته:
--passwordbox text height width [init]
لقد مرَّ معنا هذا النمط مِن قَبل في مربَّع الإدخال. النصُّ ثُمَّ الطول -الارتفاع- والعرض، ثُمَّ قيمة افتراضيّة تودُّ أن تظهر عند ظهور مربَّع الإدخال وللمستخدمِ الحُرَّيَّة في تغييرها أو تركها.
سننشِئ إخطاطة، وندمج ما تعلمناه منذ البداية حتى الآن.
سالم يُرِيدُ مِنك صُنع أداة تمكّنه من إنشاء مستخدم وتحديد كلمة مرور له، بالقدر الذي يريده، ثُمَّ تستعرض له اسم المستخدم في الملف passwd.
حاول حله قبل رؤية الحل!
الحل:
#!/bin/bash
if ! [[ $(id -u) -eq 0 ]]; then
whiptail --infobox "Run me as root" 10 97
exit 0
fi
function add_user() {
whiptail --title "Add user to the system" --ok-button "Continue" --msgbox "Hello Sir $(whoami)\
I will help you to create a user, and set a password for it, and set a shell. " 10 97
username=$(whiptail --title "USERNAME" --ok-button "Continue" --cancel-button "Exit" --inputbox "Enter Your username: " 8 85 3>&1 1>&2 2>&3)
password=$(whiptail --title "PASSWORD" --ok-button "Create user" --cancel-button "Back" --inputbox "Enter Your password: " 8 85 3>&1 1>&2 2>&3)
useradd -m -G sudo -s /bin/bash -p $(echo $password | openssl passwd -1 -stdin) $username
grep $username /etc/passwd > /tmp/out.txt
whiptail --title "simple file viewer" --ok-button "Done" --textbox out.txt 8 82
}
while :; do
add_user
if $(whiptail --title "Add User Program" --yesno "Do you want to create another user? " 8 97); then
add_user
else
break
fi;done
القوائم
القائمة menu
صَممت أداة رائعة، لديها خيارات كثيرة، تود عرض قائمة بالخيارات التي تحتويها أداتك ليختار المستخدم خيار واحد مِن تلك الخيارات، والحمد لله تقدم لك whiptail الحل على شكل خيار اسمه القائمة menu–. صيغته:
--menu text height width <menu height> tag1 item1
النصُّ، ثُمَّ الطول والعرض الخاص بالمربَّع المعروض، ثُمَّ طول القائمة، ثم الخيار الممثل بكَلِمة tag1، ثُمَّ وصف للخيار وهو ممثل بكلمة item1.
سنُنشِئ ملفًا يجعل المستخدم يختار نظامه من قائمة بها عدة أنظمة لكي يُحدِّث نظامه:
whiptail --title "Update Your System" --menu "Choose a system from the list: " 25 78 15 "1" "Debian" "2" "ArchLinux" "3" "VoidLinux"

إن كان لديك قائمة طويلة وتريد تحقيق استفادة كُلية من حجم المِطراف المفتوح فتستطيع استعمال:
eval $(resize)
whiptail --title "Update Your System" --menu "Choose a system from the list: " $LINES $COLUMNS $(( $LINES - 8 )) "1" "Debian" "2" "ArchLinux" "3" "VoidLinux"
الغرض من الأمر eval أن ينفذ المخرجات التي ستأتيه من الأمر resize.

إن أردت أن يكون المؤشر على أحد الخيارات عندما يعمل فاللاحقة default-item تفعل ذلك، اكتب اللاحقة ثُمَّ العنصر الذي تودُّ أن يكون المؤشر عليه عندما تظهر القائمة:
whiptail --title "Update Your System" --default-item "1" --menu "Choose a system from the list: " $LINES $COLUMNS $(( $LINES - 8 )) "1" "Debian" "2" "ArchLinux" "3" "VoidLinux"

وتستطيع أيضًا إخفاء الخيارات وإظهار الوصف فقط عبر اللاحقة notags–:
whiptail --title "Update Your System" --notags --menu "Choose a system from the list: " $LINES $COLUMNS $(( $LINES - 8 )) "1" "Debian" "2" "ArchLinux" "3" "VoidLinux"

قائمة التحقق check list
سيتطور برنامجك، وستريد إضافة خيارات أخرى ولن يكون من المفيد وضعها في تلك القائمة السابق ذكرها، مثلًا: تعرض قائمة لمستخدم لِيُفعَّل بعض المميزات في البرنامج، وهنا تأتي فائدة قائمة التحقق التي تُمكّن المستخدم مِن تحديد أكثر مِن خيار واحد. صيغة الأمر:
--checklist text height width <list height> <tag1> <item1> <status1>
النصُّ وارتفاع وعرض المربَّع، وارتفاع القائمة، وعنصر القائمة ثُمَّ وصف له، أما العنصر الأخير الممثل بِكَلِمة status1 الذي يُشِير إلى حالة العنصر عند عرضه، هل تريده مُفعَّل افتراضيًّا أم لا؟
إن أردته مفعَّل فضع كلمة on أما أن أردت العكس فضع كلمة off. مثال:
whiptail --title "Check List" --checklist "Choose a package you want to install: " $LINES $COLUMNS $(( $LINES - 8 )) "firefox" "web browser" off "vim" "text editor" on "XFCE" "desktop environment" off "DWM" "window manager" on

إخفاء وصف الخيار
إن أردت إخفاء وصف خيار ما، وأردت إظهار الخيار فقط، فتستطيع استخدام اللاحقة noitem–.
قائمة الراديو/المذياع radio list
تُشبِه أختها السابقة كثيرًا إلا أن الاختلاف بينهُما أن الأولى، أي قائمة التحقق، تسمح لك باختيار أكثر مِن خيار في القائمة، أمَّا قائمة الراديو/المذياع تسمحُ بتحديد خيار واحد فقط.
أظن أن اسمها جاء بسبب أن المذياع تستطيع تحديد قناة واحدة للاستماع إليها في المرة الواحدة.
صيغتها تمامًا مثل أختها:
--radiolist text height width <list height> tag1 item1 status1
مثال:
whiptail --title "Radio list" --radiolist "Choose a editor you want to install: " 28 98 18 "1" "vim" on "2" "nano" off "3" "emacs" off

مقياس التقدم gauge
يعرضُ شريط تقدم، ويقبل المدخلات مِن الدخل القياسي stdin. صيغته:
--gauge text height width [percent]
النصُّ وارتفاع وعرض المربَّع، ثُمَّ القيمة التي سيبدأ منها المقياس بالعد. مثال:
for ((i = 0 ; i <= 60 ; i+=1)); do
sleep 1
echo $i
done | whiptail --gauge "Wait until the minute is up and I will turn off the device" 6 50 0
تستطيع وضع المربَّع في أعلى الزاوية اليسرى مِن المِطراف باللاحقة topleft–.
عند تحديد مربَّع حوار ما سيبقى هناك متسع في المِطراف (ذلك الذي باللون الأزرق) فتستطيع إضافة عنوان هناك، فقد يحتوي على اسم برنامجك أو شيء آخر تريده، واللاحقة المسؤولة عن ذلك هي backtitle– متبوعًا بالنصِّ المرادِ طبعه.
الخاتمة
وها أنا أخطُّ الخطوط الأخيرة لهذا المقال، ولعلَّي وفِّقتُ في شرحهِ ولفتت انتباهُك إلى هذه البرمَجيةٍ، فهذه المقالة بذرة صغيرة لتعريفك بطريقة مختلفة للبرمجة لمحبين لغة باش.
في نهاية الأمر لا يسعني سوى شكرك لحسن قراءتك، وإني لبشر أصيب وأخطئ، فإن وفِّقتُ في طرحِ الموضوع فبتوفيق من اللّٰه عز وجل وإن أخفقت فمن نفسي والشَيطان، والسلام عليكم ورحمة اللّٰه تعالى وبركاته. دمتم بخير.
ألم تفكر كيف تعرض لك مواقع التواصل الاجتماعي المنشورات حسب اهتمامك؟
تبحث عن شيء في الفيسبوك أو تتوقف قليلًا للنظر في منشور ما عليه فتجده قد عرض لك منشورات وإعلانات عن ذلك الشيء. تشاهد مقطعًا ما عن الأكل فتجده قد عرض عليه مقاطع ومنشورات وإعلانات عن الأكل!
كل ذلك يحدث بخوارزمية برمجها مبرمج وهي مصممة لعرض المنشورات التي تهمك، تحلل كل شيء عنك لعرض المنشورات لك، مثلًا إذا فعل المستخدم كذا وكذا، وإذا توقف هنا لمدة كذا وكذا، فافعل كيت وكيت…
كل شيء تنشره عن نفسك يستخدم لتعليم الذكاء الاصطناعي وتعزيز الخوارزميات لعرض الإعلانات لك. الخوارزميات العمود الفقري للذكاء الاصطناعي الذي نراه اليوم!
الخوارزميات لا بُدَّ منها للمبرمج، فإنها:
1. تحسن طريقة تفكيره، وكتابته للرِمَاز البرمجي code.
2. وتساعد على وضوح الأفكار وتنظيمها.
3. وحل المشكلات المعقدة بفعالية وبأقل المصادر، والأخير ما تبحث عنه الشركات.
الخوارزميات تُدرَّس في الجامعات للطلبة في علوم الحاسوب. دون فهم الخوارزميات سيصعب على المبرمج فهم وكتابة البرامج، فتصبح مهمة كتابة برنامج سريع لحل مشكلة بأفضل طريقة مهمة شاقة وصعبة، بل ومستحيلة!
لقلة المصادر العربية التي تشرح الخوارزميات وأساسياتها التي يحتاجها المبرمج بتدرج ويسر، فقد قررت شرح كتاب أعجمي نفيس يُعدُّ مدخلًا رائعًا لدراسة الخوارزميات، بلغة عربية لمن لا يُجيد الإنگليزية.
الكتاب يشرح الخوارزميات بسهولة وبرسومات تسهل الفهم، اسم الكتاب «استيعاب الخوارزميات Grokking Algorithms»، وسأشرحه بمقالات منفصلة، كل مقالة تتناول موضوعًا أو أكثر، حتى ننتهي من الكتاب، وهذه المقالات ستغنيك عن قراءة الكتاب.
متطلبات الدورة
كل ما تحتاجه للبدء معرفة بأساسيات لغة برمجية، مثل لغة python أو C أو أي لغة أخرى، لكن الأمثلة المضروبة في الكتاب مكتوبة بلغة python لكني سأضيف أيضًا أمثلة بلغة C.
أما المعرفة الرياضية فتكفي معرفة الأساسيات من ضرب وقسمة وطرح وجمع، وأيضًا الأسس، وكلما أحتجنا لمفهوم رياضي فإني سأشرحه لك قبل البدء في الدرس، كما سأفعل اليوم بشرح الأسيس (تعريب كلمة اللوغارثيم logarithm).
نتوكل على الله ونبدأ!
الأسيس
هل تتذكر الأسس؟
الأس هو عدد المرات التي يضرب فيها الأساس في نفسه، مثلًا الرقم 5²، يُقرأ خمسة أس اثنين، ورقم خمسة هو الأساس، واثنين أس خمسة يعني ضرب الرقم خمسة في نفسه مرتين:
5x5 = 25
إذن فإن خمسة أس اثنين 5² يساوي 25، وبالمثل فإن 10³ يساوي:
10x10x10 = 1000
إذن فإن عشرة أس ثلاثة يساوي ألف (10³ = 1000).
حل هذه الأمثلة قبل مواصلة القراءة:
8³ = ?
4² = ?
5³ = ?
9² = ?
حسنًا، ما الأسيس (اللوغاريثم logarithm)؟
هو صورة مختلفة للأس فقط!
التعريف يقول: أسيس (لوغاريثم) أي عدد لأساس معلوم هو الأس الذي يُرفَع له الأساس المعلوم كي يعطينا العدد.
إن فهمت التعريف فخير وبركة وإن لم تفهمه فتابع الشرح.
تذكر هذا المعادلة جيدًا:
5² = 25
لنطبق التعريف على المعادلة المذكورة آنفًا (5² = 25):
أسيس العدد 25 (لأن العدد في معادلتنا هو 25)، للأساس المعلوم 5 (لأن الأساس في معادلتنا هو 5)، هو الأس الذي يُرفَع له الأساس المعلوم (أي 5) كي يعطينا العدد (أي العدد 25)، إذن ما هو الأس الذي إذا رفعنا الأساس 5 إليه أعطانا العدد 25؟
عليك نور، هو الأس 2، فلو رفعنا الأساس 5 إلى الأس 2 فإن العدد الذي سنحصل عليه هو العدد 25.
هل فهمت الآن لماذا قلت إن الأسيس هو صورة مختلفة للأس فقط؟
فنحن نحاول إيجاد الأس الموضوع فوق الرقم 5 الذي سيعطينا (إذا ضربنا الرقم 5 في نفسه حسب الأس الموضوع) العدد 25 فقط.
وبعبارة أسهل وأوضح، فنحن نقول: ما الأس الذي إذا رفعناه للأساس 5 سيعطينا العدد 25؟
والجواب سيكون: الرقم اثنين، فإن 5×5 يساوي 25.
ولكتابة الأسيس بصيغة رياضية فإنه يمثل بالعربية بالرمز (لو):
لو5 (25) = 2
وبصيغة إنگليزية:
log5 (25) = 2
ونقرأه: أسيس العدد 25 للأساس المعلوم 5، فأن العدد هو 25، والأساس المعلوم هو 5.
لو: لوغاريثم logarithm، وهي كلمة أعجمية عُرِّبت إلى أسيس، ولكن ساد وغلب المصطلح الأعجمي لوغاريثم logarithm، ومنه أُخِذَ (لو).
مثال آخر:
حل المسألة التالية:
لو2 (8) = ؟
log2(8) = ?
يُقرَأ: أسيس العدد 8 للأساس المعلوم 2 يساوي كم؟
أي: ما الأس الذي إذا رفعناه للرقم 2 سيعطينا العدد 8؟
الحل سيكون الأس 3، فلو رفعنا الأساس 2 للرقم 3 فإنه سيعطينا العدد 8، لأن ضرب الرقم 2 في نفسه ثلاث مرات (2×2×2) يساوي 8:
log2 (8) = 3
لو2 (8) = 3
ويقرأ: أسيس (لوغاريثم) العدد ٨ للأساس المعلوم ٢ يساوي ٣.
أسئلة لاختبار فهمك:
أ. أي من الاختيارات الأربعة يعادل 32 = 2⁵:
1. لو2 (32) = 5
2. لو5 (2) = 35
3. لو32 (5) = 2
ب. أي من الاختيارات الأربعة يعادل 125 = 5³:
1. لو3 (125) = 5
2. لو5 (125) = 3
3. لو125 (5) = 3
ج. اكتب لو4 (16) = 2 بصيغة أسيَّة.
د. اكتب لو2 (64) = 6 بصيغة أسيَّة.
هـ. حل المسائل التالية:
لو6 (36) = ؟
لو3 (27) = ؟
لو4 (4) = ؟
لو1 (5) = ؟
الخوارزمية
الخوارزمية: مجموعة الخطوات الدقيقة والمنطقية المتسلسلة لحل مشكلة ما.
يمكن تسمية كل تعليمة من الرِمَاز البرمجي بخوارزمية، لأن الرِمَاز البرمجي كاملًا ليس إلا خوارزمية لحل مشكلة ما.
كلمة «خوارزمية» في ذاتها لا توضِّح معناها لهذا عرفتها في البداية. الخوارزمية مشتقة من اسم محمد بن موسى الخوارزمي (من عام ٧٨٠ إلى عام ٨٥٠ تقريبًا)، وهو عالِم رياضيات وفلك مسلم وهو أول من ابتكرها في القرن التاسع الميلادي.
تعدَّدت إسهامات الخوارزمي وانتشرت على نطاق واسع. فمصطلح «الجبر» مشتقٌّ من العنوان العربي لأكثرِ كتبه تأثيرًا وهو كتاب «المختصر في حساب الجبر والمقابلة».
أُدخل اسم الخوارزمي إلى اللغة اللاتينية وصار Algorismus والذي أصبح يشير إلى طريقة الحساب العددي باستخدام الأعداد العشرية. تأثَّر المصطلح اللاتيني Algorismus بالكلمة اليونانية arithmos وتعني «العدد» (مثل كلمة arithmetic وتعني علم الحساب)، ومِن ثَم أصبحت algorithm بمعنى خوارزمية، وظلت تشير إلى العمليات الحسابية العشرية، قبل أن تكتسب معناها الحديث في القرن التاسع عشر.
عندما تُعدُّ كأس قهوة فإن الخطوات الدقيقة والمتسلسة لحل لتحضير القهوة تسمى خوازرمية، فالخوارزميات نستخدمها في حياتنا اليومية بداهةً، فتح الباب وغسل الملابس وغيرهما…
خوارزمية البحث الثنائي والبدائي
لنفترض أنك ترغب بالبحث عن شخص اسمه كمال في دليل الهاتف القديم، وبما أن اسمه كمال فإنك ستجد رقمه في الأسماء التي تبدا بحرف الكاف. لن تفتح الكتاب من البداية وتبدأ بتقليب الصفحات صفحة صفحة لتصل إلى حرف الكاف، بل قد تفتح دليل الهاتف من النصف آملًا أن تكون قريبًا من حرف الكاف، فهذا أسهل وأسرع.
ولنفترض أنك تبحث عن معنى كلمة قاتل في المعجم الوسيط، فأنك لن تفتحه وتبدأ بالبحث عن الكلمة في كل صفحة حتى تصل إليه، فإن قاتل من الجذر قَتَلَ، فستفتح باب حرف القاف مباشرةً إن كنت تعرف صفحته، ولكن ردة الفعل الطبيعية أن تفتح المعجم من منتصفه لترى هل أنت قريب من حرف القاف؟
إن كان حرف القاف بعيدًا فأنك ستستمر بنفس الحركة السابقة، أن تفتح المعجم الوسيط من منتصفه المتبقي مرة أخرى، وهذا يختصر عليك الجهد والوقت، وأقصد من منتصفه المتبقي المنتصف الذي يتواجد به حرف القاف، فإن فتحت المعجم من منتصفه وكان الحرف الذي أمامك حرف السين فأنك ستفتحه مجددًا من الحروف التالية، التي تلي حرف السين.
ولنفترض أنك تسجل في حسابك على الفيسبوك، فإن الفيسبوك يتحقق من وجود حسابك على الموقع بالبحث في قاعدة بياناته عنه. لو كان اسم مستخدمك هو سعيد Saed فخل تتوقع أن يبدأ بالبحث عنه من البداية من حرف الألف A حتى آخر حرف وهو الذي يمتلك مستخدمين يصل عددهم إلى آلاف مؤلفة؟!
من المنطقي أن يبدأ بالبحث عنه في مكانٍ ما في الوسط، فهذا أسرع وأسهل.
مما سبق يتضح لك أن ما واجهناه يسمى مشكلة بحث، وكل الافتراضات السابقة تستخدم نفس خوارزمية البحث لحل المشكلة: خوارزمية البحث الثنائي binary search.
سميت بخوارزمية البحث الثنائي لأنها تقسم الائحة المراد البحث داخلها عن عنصر ما إلى نصفين مرارًا وتكرارًا حتى تجد العنصر المطلوب.
خوارزمية البحث الثنائي تستقبل لائحة مرتبة من العناصر sorted list، ويجب أن تكون اللائحة مرتبة. إن كان العنصر الذي تبحث عنه في اللائحة متواجدًا فيها فإن خوارزمية البحث الثنائي سترجع موقع العنصر حيث يُخزَّن، وإن لم تجده ستعيد قيمة فارغة أو خالية null.
أظنك فهمت القيمة الفارغة null بما أنك مبرمج.
مثلًا لو وجدت خوارزمية البحث الثنائي اسم سعيد فإنها ستعيد موقعه حيث يُخزَّن، لنفترض أن موقعه 1400، أما إن لم تجده فإنها ستعيد إرجاع قيمة فارغة null.
لنلعب لعبة!
سأفكر برقم محصور بين الواحد والرقم مائة، وأنت ستخمن الرقم في أسرع محاولات ممكنة. في كل تخمين ستقوله سأخبرك إذ كان الرقم الذي خمنته صحيحًا أو يكبر الرقم الذي أفكر به too high، أو يصغره too low.
سأفترض أنك أردت أن تتذاكى عليَّ وخمنتَ بالطريقة البدائية التقليدية هكذا: واحد، اثنان، ثلاثة، أربعة، خمسة، إلخ…
سيبدو كالرسومات التالية:
تقول: واحد؟
أجيبك: لا، بل يكبره.
تقول: اثنان؟
أجيبك: لا، بل يكبره.
الطريقة التي استعملتَها في تخمين الرقم الذي أفكر به رقمًا رقمًا يسمى بخوارزمية البحث البدائي أو التقليدي حسب ما يسميها الكاتب في كتابه، وتسمى أيضًا خوارزمية البحث الخطية liner search، وهذه الخوارزمية بطيئة فأنك محدود برقم واحد في كل تخمين، وستحتاج إلى تسعة وتسعين مرة تخمين إن كان الرقم الذي أفكر به تسعة وتسعين، أليس هذا متعبًا؟
لن ألعب معك إن كنت ستضيع الوقت هكذا!
لنتبادل الأدوار في اللعب، أنت تفكر برقم وأنا أخمنه، سأريك كيف أن خوارزمية البحث الثنائي binary search أسرع من خوارزمية البحث البدائي simple search. لنفترض أن الرقم الذي تفكر به هو 57:
سأبدأ بتقسيم اللائحة المرتبة إلى نصفين (أتفقنا أن الرقم محصور بين الرقم واحد ومائة، إذن فإن اللائحة مرتبة!)، وسأبدأ من الرقم خمسين 50.
أقول: أهو الرقم 50؟
تُجيب: خطأ، يكبره.
انظر كيف خمنت أكثر من خمسين رقمًا في مرة واحدة، بما أنه يكبره فلا يعقل أن أعيد وأقول لك: أهو الرقم 47؟
لأني أعلم بأن الرقم يكبر الذي تفكر به يكبر الرقم 50. هيا سأكمل وأقسم الجزء المتبقي إلى نصفين مرة أخرى.
أقول: أهو الرقم 75؟
تُجيب: خطأ، يصغره!
يصغر الرقم 75 لكني تخلصت من خمسة وعشرين رقمًا في تخمين واحد!
خوارزمية البحث الثنائي تقسم الائحة مرارًا وتكرارًا إلى نصفين، وفي كل مرة تختصر نصف اللائحة. هيا، لنستمر بالتخمين، هذه المرة سأقول 63، لأن الرقم في منتصف الأرقام بين 50 و75 هو 63.
أقول: أهو الرقم 63؟
تُجيب: أخطأت، يصغره!
أقول: أهو الرقم 57؟
تُجيب: أحسنت، صحيح!
الرقم الذي ينتصف الأرقام بين 63 و50 هو 57!
هنيئًا لك، ها أنت تعلمت خوارزمية بحثك الأولى، خوارزمية البحث الثنائي!
لا يهم أي رقم فكرت به حتى وإن كان الرقم مائة، فإني سأخمنه بتخمينات أقصاها سبعة تخمينات فحسب.
اسأبدأ التخمين من الرقم مائة، من النهاية، والرقم الذي أفكر به هو الرقم واحد، انظر كم سيأخذ الأمر مني محاولات، سبعة تخمينات فقط:
مثال ثانٍ
لنفترض أنك تبحث عن كلمة في قاموس عدد كلماته 240,000 كلمة. في أسوأ حالة worst case، وأسوأ حالة أن تكون الكلمة في نهاية القاموس (اللائحة)، كم خطوة ستأخذ كل خوارزمية بحث من خوارزميتي البحث الثنائي والبدائي لإيجاد كلمة ما في القاموس الذي عدد كلماته 240,000 كلمة؟
ستأخذ خوارزمية البحث البدائي 240,000 خطوة إذا كانت الكلمة في نهاية القاموس، لكن في خوارزمية البحث الثنائية ستقسم عدد الكلمات إلى نصفين، وهكذا سننتهي بالكلمة المطلوبة، وستأخذ خوارزمية البحث الثنائية 18 خطوة فقط!
أليس الفرق شاسعًا بينهما؟
أظنك تتساءل لو كان لديك رقم ضخم، لنقل ألف ألف رقم (million)، هل ستأخذ الورقة والقلم وتبدأ بتقسيمه كل مرة إلى نصفين لتعرف عدد الخطوات؟
أبدًا لا، هنا يأتي دور الرياضيات، وتحديدًا ما تعلمته في بداية الدرس، أنه الأسيس!
القاعدة تقول: لأي لائحة من س، فإن خوارزمية البحث الثنائية ستستغرق لو2 (س) خطوة في أسوأ حالة in the worst case، أما خوارزمية البحث البدائية ستستغرق س خطوة في أسوأ حالة.
استعمل الآلة الحاسبة لإيجاد حل المسألة الرياضية السابقة (لو2 (س)).
يرجى الإنتباه، سيكون الأساس للأسيس دائمًا هو 2، فإني في بقية المقالات سأستعمل (لو) متبوعًا بحجم اللائحة، ولن أكتب الأساس 2. لكن لماذا الأساس المعلوم هو الرقم 2؟
لأن الحاسوب لا يفهم إلا الآحاد والأصفار، نظام العد الثنائي binary system فقط الذي أساسه الرقم 2.
لنحاول تطبيق القاعدة:
لدية لائحة عدد عناصرها 1024 عنصرًا، إذا طبقنا القاعدة فإن:
لو (1024) = 10
أتفقنا مسبقًا أن الأساس سيكون دائمًا الرقم 2 ولن أكتبه في بقية الدروس القادمة.
تمثيل خوارزمية البحث الثنائي برمجيًا
حان الوقت لنبرمج أول خوارزمية بحث تعلمتها. سنحتاج في رِمَازنا البرمجي إلى الصفيفَات arrays، إن كنت لا تعلم ماهيتها فلا تقلق، فإني سأشرحهها في الفصل الثاني من الكتاب.
المطلوب منك فهمه عن الصفيفة array أنها متتالية من عناصر من نفس نوع البيانات، في مواقع ذاكرة متجاورة.
الفهرسة تبدأ في الصفيفة من الرقم صفر، فموقع أول عنصر فيها يمثل بالرقم صفر، وثاني عنصر بالرقم واحد، وهكذا…
سننشئ وظيفة function ونسميها binary_search تأخذ صفيفة مرتبة sorted array وعنصر. فإن كان العنصر الذي أخذته الوظيفة موجودًا في الصفيفة التي أستقبلتها فإنها سترجع موقعه، وإن لم يكن موجودًا سترجع قيمة فارغة null.
أولًا علينا معرفة طول المصفوفة، لنحدد الرقم الذي تبدأ منه والرقم الذي تنتهي عنده:
low = 0
high = len (list) – 1
الآن بعدما حددنا بدايتها ونهايتها علينا تقسيمها إلى نفصين في كل مرة، ثم نخزن العنصر الذي ينتصف الصفيفة في متغير لمقارنته لاحقًا:
mid = (low + high) / 2
guess = list[mid]
الآن علينا اختبار العنصر الذي خزناه بالعنصر المعطى عند استدعاء الوظيفة، هل يساويه أم يصغره أو يكبره.
if guess < item:
low = mid + 1
هذا الرِمَاز البرمجي كاملًا مكتوب بلغة python:
def binary_search(lis, item):
low = 0
high = len(lis) – 1
while low <= high:
# … check the middle element
mid = (low + high) // 2
guess = lis[mid]
# Found the item.
if guess == item:
return mid
# The guess was too high.
if guess > item:
high = mid – 1
# The guess was too low.
else:
low = mid + 1
# Item doesn’t exist
return None
my_list = [1, 3, 5, 7, 9]
print(binary_search(my_list, 3))
print(binary_search(my_list, -1))
هذا رِمَاز برمجي مكتوب بلغة C:
#include
int binarySearch(int[], int, int);
int main()
{
int myList[] = {1, 3, 5, 7, 9};
int len = sizeof(myList) / sizeof(myList[0]);
printf(“%d\n”, binarySearch(myList, 3, len)); // 1
printf(“%d\n”, binarySearch(myList, -1, len)); //-1
return 0;
}
int binarySearch(int list[], int item, int len)
{
int low = 0;
int high = len;
while (low <= high)
{
int mid = (low + high)/2;
int guess = list[mid];
if (guess == item)
{
return mid;
}
else if (guess > item)
{
high = mid – 1;
}
else
{
low = mid + 1;
}
}
return -1; //number not found
}
تمارين
1. عندك لائحة مرتبة من 128 اسمًا، فقررت أن تبحث فيها عن اسم ما باستخدام خوارزمية البحث الثنائي، فما أقصى (أي: أسوأ حالة) عدد من الخطوات تحتاجها للعثور عليه؟
2. ضاعفت اللائحة السابقة، أي ضعف العدد 128، فما أقصى عدد من الخطوات تحتاجها للعثور على الاسم؟
عليك حلها!
الخاتمة
لن تكون هنا نهايتنا بل بدايتنا لسلسلة متكاملة وشاملة عن الخوارزميات، وسأشرح الكتاب كاملًا، وستغنيك المقالات المنشورة عنه عن قراءته.
وها أنا أخط بقلمي الخطوط الأخيرة لهذا المقال الشائق، وأرجو إني قد وفِّقت في الشرح.
وفي نهاية الأمر لا يسعني سوى أن أشكرك على حسن قراءتك لهذا المقال، وأني لبشر أصيب وأخطِئ، فإن وفِّقت في طرح الموضوع فمن اللّٰه عز وجل وإن أخفقت فمن نفسي والشيطان.
أرجو منك تقييم كفاءة المعلومات من أجل تزويدي بالملاحظات والنقد البناء في خانة التعليقات أو عبر حساب الموقع، والسلام عليكم ورحمة اللّٰه تعالى وبركاته.