Uploaded avatar of rpalo

البرمجة المتعمّدة في تمرين Grains بلغة Bash

@rpalo
منذ أكثر من 7 سنوات

تنبيه: تحتوي هذه المقالة على حرق لتمرين حبوب بشكل عام، وبشكل خاص لتمرين حبوب على مسار Bash. إذا لم تكن قد أكملته بنفسك بعد، ولا تريد أن تُعرض عليك بعض الحلول، فارجع إليها بعد أن تنتهي منه!

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

ما الذي يحدث في هذا الكود؟

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

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

الفكرة القائلة إن تفاصيل التنفيذ يجب أن تساعد قارئ الكود على استبيان العملية الفكرية والأهداف والأولويات تُسمّى نيّة التصميم. كيف تسمّي متغيراتك، وما المعاملات التي تأخذها دالتك، وكيف تُجرّد الأمور، كلها مواضع يمكن فيها التعبير عن نيّة التصميم، سواء بإتقان أو بسوء.

أؤمن إيمانًا راسخًا بأن نيّة التصميم من أهم الأمور التي يجب مراعاتها عند تنفيذ تصميم هندسي. إنها أحد الأشياء التي تميّز هندسة البرمجيات عن البرمجة.

هندسة البرمجيات هي ما يحدث للبرمجة عندما تضيف الوقت ومبرمجين آخرين.

Russ Cox

نيّة التصميم عابرة للتخصصات

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

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

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

عندما يخرطون هذا الثقب ويقيسون كيف ظهر، إذا رأوا رقمًا مثل 5.99mm، سيفكرون: "حسنًا، كان من المفترض أن يكون 6mm على الأرجح، فأنا قريب جدًا"، ولن يضطروا حتى للعودة والتحقق من الأبعاد على CAD أو رسم المواصفات. بدلًا من ذلك، لو جعلته شيئًا غير شائع، مثل 5.87mm، سينظرون إليه وتكون ردّة فعلهم الأولى:

  1. أوه، هل جاء مقاسي أصغر بكثير من اللازم؟ هل كان من المفترض أن يكون 6mm؟
  2. (يذهبون للتحقق من CAD ويرون أن ثقبهم جيد وأنه مجرد حجم غير شائع.)
  3. هممم. أنا متأكد أن هذا الثقب بحجم غير شائع لسبب ما. ربما هو مهم حقًا، أو طلب العميل ثقبًا خاصًا هنا. سأحتاج للذهاب والتحدث مع Ryan لأرى ما المهم في هذا الثقب.
  4. (دوووووي! يضعون قطعة الألمنيوم على مكتبي بلطف.)
  5. (يكتشفون أنه لا شيء مهم في هذا الثقب، أنا فقط اخترت حجمًا غريبًا، وكل هذا العمل والقلق الإضافي كان بلا سبب.)
  6. يا له من رجل، ذلك Ryan، إنه شخص مزعج حقًا. (دمدمة، شتيمة، دمدمة)

كل هذا يحدث لأن كل قرار في تصميمي يوصل شيئًا للآخرين الذين ينظرون إليه ويعملون عليه، سواء قصدت ذلك أم لا. لا بد أن يروا معنى فيه، لأنه المعلومات الوحيدة المتاحة لهم! لذا، من الأفضل بكثير أن آخذ وقتي لأضع معلومات ذات معنى ومقصودة في تصميمي.

حبوب: مقدمة

الآن، لنتحدث عن كيفية إيصال نيّة التصميم في الكود، باستخدام مثال من أحد تمارين Exercism. عملت مؤخرًا مع طالب على حلّه لتمرين حبوب على مسار Bash. حبوب تمرين يتناول مسألة القمح ورقعة الشطرنج. باختصار، تُوضع حبة قمح واحدة على المربع الأول من رقعة الشطرنج. وتُوضع حبتان على المربع التالي. وأربع حبات على المربع التالي. وهكذا، بحيث يحتوي كل مربع على ضعف حبوب المربع السابق. يُطلب من الطلاب إيجاد وسيلة لحساب القيمة على كل مربع على حدة، وكذلك الإجمالي الكلي لعدد الحبوب على الرقعة.

توصّل هذا الطالب تحديدًا إلى وسيلة ذكية جدًا لحساب الإجمالي.

bc <<< 'ibase=16;FFFFFFFFFFFFFFFF'

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

يعمل هذا الحل لأن التمرين كله يدور حول قوى العدد اثنين، وحيثما توجد قوى الاثنين يوجد النظام الثنائي، وحيثما يوجد النظام الثنائي يوجد النظام السادس عشري2!

إنه حل ذكي، لكن ما الذي يقوله لنا الكود؟ أن النظام السادس عشري مهم هنا؟ أن المسألة تدور أساسًا حول العدد 16؟ بعد إعادة قراءة نص المسألة، يتضح تمامًا أن أيًّا من ذلك ليس هو الحال. عصفنا ذهنيًا أنا والطالب معًا على بعض الأفكار لإيصال النيّة بوضوح أكبر. إليك بعض ما توصّلنا إليه:

الخيار الأول: النظام الثنائي

لأن لدينا مجموعة من الأشياء تتضاعف، وبالتالي مجموعة من قوى العدد 2، لننظر إلى ما يحدث في النظام الثنائي لنرى إن كان ذلك سيساعدنا.


المربع الأول عليه حبة واحدة. في النظام الثنائي، سيكون هذا 0b1 أيضًا (حيث تعني 0b فقط "هذا عدد ثنائي"، والعدد الفعلي هو 1).

المربع الثاني عليه حبتان. في النظام الثنائي، 0b10. والمجموع حتى الآن هو 3 (أو 0b11).

المربع الثالث عليه 4 (0b100) حبات. المجموع حتى الآن: 7 (0b111).

المربع الرابع عليه 8 حبات (0b1000). المجموع حتى الآن: 15 (0b1111).


هل ترى النمط؟

يمثّل كل مربع رقمًا ثنائيًا آخر، وجمعها كلها معًا يعطي مجموعة من الآحاد.

في حل الطالب، يمكننا استبدال حروف F بـ 64 واحدًا (واحد لكل مربع)!

bc <<< "ibase=2;1111111111111111111111111111111111111111111111111111111111111111"

أكثر قصدًا، لأنه يطابق ما تعطينا إياه المسألة عن قرب أكبر. لكننا لا نتحدث لغة الروبوتات. تسلسل طويل من الآحاد يكاد لا يُعدّ قد لا يكون تحسينًا.

الخيار الثاني: الحساب بالقوة الغاشمة

حسنًا، ربما نتخلى عن أنظمة العدّ غير العشرية تمامًا. لماذا لا نجعل الكود يطابق كيف كنا سنجمع عدد الحبوب على رقعة الشطرنج يدويًا، بعدّ الحبوب على كل مربع؟

total=0
current_grains=1
for square in {1..64}; do
  total=$( bc <<< "$total + $current_grains" )
  current_grains=$( bc <<< "$current_grains * 2" )
done
echo "$total"

هذا أكثر قابلية للقراءة والفهم بكثير. يُظهر الكود بوضوح أن عدد المربعات على رقعة الشطرنج عامل مؤثر، وكذلك تأثير التضاعف في كل مربع. أعتقد أن هذا أفضل من الحل الأولي.

لكن.

إنه بطيء. التكرار، والجمع، والاستدعاء المتكرر لأمر خارجي؟ كلها تتراكم لتعطي زمن تشغيل بطيئًا نوعًا ما. فهل هذا أمر كبير؟ لا. إذا كنت تكتب هذا كسكربت في Bash، فغالبًا قررت مسبقًا أنه لا توجد قيود على السرعة لديك. لكن هل يمكن أن يكون أفضل؟ نعم.

الخيار الثالث: الحساب المباشر

إذن، كيف نجمعها كلها دون تكرار؟

لنفكر في نسخة أصغر من المسألة نفسها: رقعة شطرنج بخمسة مربعات3.

سيكون لدى المربعات الخمسة العدد التالي من الحبوب:

---------------------
| 1 | 2 | 4 | 8 |16 |
---------------------

والمجموع هنا سيكون: 1 + 2 + 4 + 8 + 16 = 31. همم. لا يصرخ 31 في وجهي بأي شيء واضح بعد. لنجرّب حجمًا أكبر قليلًا.

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

-------------------------
| 1 | 2 | 4 | 8 |16 |32 |
|   | 3 | 7 |15 |31 |63 |
-------------------------

والمجموع: 1 + 2 + 4 + 8 + 16 + 32 = 63. همم... بدأت فعلًا أرى وميضًا من نمط، لكننا سنفعل واحدة أخرى للتأكد.

7 مربعات:

-----------------------------
| 1 | 2 | 4 | 8 |16 |32 |64 |
|   | 3 | 7 |15 |31 |63 |127|
-----------------------------

1 + 2 + 4 + 8 + 16 + 32 +64 = 127. هل ترى ذلك؟ هل أي شيء يدقّ ناقوس الخطر مع القيم 31 و63 و127؟

إنها تقريبًا قوى العدد 2. بل إنها أقل بواحد من القوة التالية للعدد اثنين.

مثال آخر، لتثبيت الفكرة. تخيّل رقعة شطرنج بـ 12 مربعًا. هذا واحد مضاعف 11 مرة (وهو، في عالم الرياضيات، 2^11): 2048. ضاعف ذلك مرة أخرى، فتحصل على 4096 (2^12). إذن... إذا كنا أصبنا النمط، فسيكون المجموع الجاري أقل بواحد من 4096، المعروف أيضًا بـ 4095. وإذا جمعناه، فهذا بالضبط ما نحصل عليه: 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 + 256 + 512 + 1024 + 2048 = 4095.

بعبارة أخرى، لإيجاد المجموع لكل n مربع، عليك الصعود قوة واحدة للعدد اثنين وطرح 1 من الناتج.

عدد الحبوب على المربع 64 هو 2^63 (الفهرسة تبدأ من الصفر، أتذكر؟). إذنننننن، إذا أردنا حساب إجمالي الحبوب على كل المربعات حتى المربع 64 شاملًا، علينا حساب 2^64 وطرح 1.

بام!

في Bash، سيبدو هكذا:

bc <<< "2^64 - 1"

هذا منطقي عندما تؤكد ما يحدث في النظام الثنائي. في النظام الثنائي، ما كان مجموع كل المربعات الـ 64؟

0b1111...  # 64 ones

ما عدد الحبوب على المربع الخامس والستين النظري؟

0b10000... # 1 and 64 zeros

كيف تنتقل من 1 و64 صفرًا إلى 64 واحدًا؟ تطرح 1.

وما الفائدة الإضافية التي يمنحنا إياها هذا؟ حسنًا، الآن لدينا تعبير جميل ومقروء للإجمالي. إنه لا يكرّر، فالأداء جيد. ويحتوي على العدد 64، وهو عدد المربعات على رقعة الشطرنج، وهو مثال جيد على نيّة التصميم المُشار إليها جيدًا. إذا ما اتّخذ العالم لسبب ما، بعد 1000 سنة، رقعة شطرنج بقياس 7×7 معيارًا، فسيتحقق ذلك المهندس المستقبلي (الذي يستخدم على الأرجح Bash 6.1) من السكربت، ويرى ما كنت تسعى إليه، ويغيّر 64 إلى 49. كل شيء جيد!

ابقَ مقصودًا، يا أصدقائي

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

  1. انظر أيضًا القصة المصوّرة لـ Thom Holwerda. ↩

  2. إذا كنت تشعر بأنك صدئ قليلًا في العدّ الثنائي والسادس عشري، فإن @kytrinyx يوصي بكتاب How to Count. وكإعلان بلا خجل، كتبت مؤخرًا تدوينتين في المدونة عن النظام الثنائي والسادس عشري أيضًا. ↩

  3. لا أعرف كيف سيعمل ذلك. ربما يمكننا ببساطة أن نجعل البيادق تتناطح ببعضها البعض. ↩

14 فبراير، 2019 · هل وجدته مفيدًا؟