Uploaded avatar of rpalo

کدنویسی هدفمند در تمرین دانه‌ها با Bash

@rpalo
بیش از 7 سال پیش

هشدار اسپویل: این مقاله برای تمرین Grains به‌طور کلی، و به‌طور خاص تمرین Grains در مسیر Bash، اسپویل دارد. اگر هنوز خودتان آن را کامل نکرده‌اید و نمی‌خواهید چند راه‌حل به شما نشان داده شود، بعد از تمام کردنش برگردید!

اولین روزتان در یک شرکت جدید است. کارهای اداری را انجام داده‌اید، با تیم آشنا شده‌اید، و حالا بالاخره وقت آن است که بنشینید و شروع کنید به خواندن بخشی از کدی که قرار است روی آن کار کنید. شروع می‌کنید به خواندن توابع، کلاس‌ها و ماژول‌های مختلف، و همان‌طور که می‌خوانید، متوجه می‌شوید که از سردرگمی، چشمتان را به صفحه ریز می‌کنید. ادامه می‌دهید و کلمه‌ای از دهانتان بیرون می‌آید، به‌سختی ادا شده، تقریباً در حد یک نفس: «چییییییی...»1 هرچه بیشتر پیش می‌روید، این بیشتر اتفاق می‌افتد و بی‌قرارتر و حتی کمی عصبانی‌تر می‌شوید.

در این کد چه اتفاقی می‌افتد؟

هر زمان بیش از یک نفر روی قطعه‌ای کد کار می‌کند، میزان دقت و هدفمندی‌ای که برای قابل مدیریت نگه داشتن اوضاع لازم است بسیار بالا می‌رود. دیگر این‌طور نیست که مفهوم در ذهن شما زندگی کند و کد فقط باید آن مفهوم را محقق کند. حالا، مفهوم باید درون خود کد زندگی کند، جایی که همه‌ی همکاران بتوانند آن را ببینند و در صورت لزوم تغییرش دهند.

اینکه چگونه چیزی را پیاده‌سازی می‌کنید برای کاربر نهایی معنای زیادی ندارد، اما باید برای هر مهندسی که هر لحظه به طراحی شما دست می‌زند، حرف‌های زیادی برای گفتن داشته باشد. اغلب راه‌های زیادی برای رسیدن به یک کارکرد یکسان وجود دارد، و ممکن است به نظر برسد هر کدام از این گزینه‌ها برای انجام کار کافی است. با این حال، من معتقدم هر تصمیم تک‌تک شما باید دلیلی داشته باشد (حتی اگر تصمیمی کوچک با دلیلی کوچک باشد)، و آن دلیل باید یک هدف یا یک نیاز را منتقل کند.

این ایده که جزئیات پیاده‌سازی باید به خوانندگان کد کمک کند روند فکر، اهداف و اولویت‌ها را تشخیص دهند، قصد طراحی نامیده می‌شود. اینکه متغیرهایتان را چه می‌نامید، تابع شما چه پارامترهایی می‌گیرد، و اینکه چیزها چگونه انتزاع می‌شوند، همه جاهایی هستند که قصد طراحی می‌تواند بیان شود، چه خوب و چه بد.

من قویاً معتقدم که قصد طراحی یکی از مهم‌ترین مواردی است که هنگام پیاده‌سازی یک طراحی مهندسی باید در نظر گرفت. یکی از مواردی است که مهندسی نرم‌افزار را از برنامه‌نویسی جدا می‌کند.

مهندسی نرم‌افزار چیزی است که وقتی زمان و برنامه‌نویسان دیگر را به برنامه‌نویسی اضافه کنید، بر سرش می‌آید.

Russ Cox

قصد طراحی، فراتر از یک رشته

من به‌عنوان یک مهندس مکانیک کار می‌کنم و قالب‌های تزریق طراحی می‌کنم، بیشتر برای دستگاه‌های پزشکی. همه‌ی طراحی‌های من، وقتی تمام می‌شوند، مستقیم از در بیرون می‌روند به کارگاه ماشین‌کاری، جایی که شروع می‌کنند به ساختن همه‌ی قطعات و سوار کردنشان روی هم. چون آن‌ها نمی‌دانند در ذهن من هنگام ساخت هر طراحی چه گذشته است، باید راهی پیدا کنم که قصد خود را از طریق خود طراحی نشان بدهم.

خیلی وقت‌ها، بعضی ویژگی‌ها به‌طور خاص حیاتی‌اند. یا مشتری گفته که به تلورانس‌های دقیق ویژه‌ای در آنجا نیاز دارد، یا نحوه‌ی جفت شدن قالب به دلیلی به دقت بسیار بالایی نیاز دارد. بنابراین، برای اینکه به ماشین‌کارها کمک کنم قطعات را به شکلی بسازند که دقت روی بخش‌های مهم اولویت داشته باشد، باید جاهایی بگذارم که دقیقاً گونیا باشند یا راحت بشود به شکلی خاص در گیره گذاشتشان. این‌طور، ساده‌ترین مسیر برای آن‌ها بهترین نتیجه را برای من تولید می‌کند.

جاهایی هم هستند که ابعاد آن‌قدر حیاتی نیستند. مثلاً، اگر در طراحی سوراخی بگذارم که فقط برای هواکش است، آن را یک اندازه‌ی رایج و مرتب می‌سازم، مثل ۶ میلی‌متر.

وقتی این سوراخ را ماشین‌کاری می‌کنند و می‌روند اندازه بگیرند که چطور از آب درآمده، اگر عددی مثل ۵٫۹۹ میلی‌متر ببینند، فکر می‌کنند: «خب، این احتمالاً باید ۶ میلی‌متر می‌بود، پس تقریباً نزدیکم» و دیگر حتی لازم نیست بروند ابعاد را در CAD یا نقشه‌ی مشخصات دوباره بررسی کنند. اما اگر آن را چیزی غیررایج بسازم، مثل ۵٫۸۷ میلی‌متر، به آن نگاه می‌کنند و این واکنش اولیه را دارند:

  1. اوه، ای دل غافل، آیا خیلی کوچک‌تر از حد درآمدم؟ باید ۶ میلی‌متر می‌بود؟
  2. (می‌روند CAD را بررسی می‌کنند و می‌بینند سوراخشان درست است و فقط یک اندازه‌ی غیررایج است.)
  3. هوم. مطمئنم این سوراخ به دلیلی اندازه‌ی غیررایجی دارد. شاید واقعاً مهم است، یا مشتری سوراخ خاصی اینجا خواسته. باید بروم با رایان صحبت کنم و ببینم چه چیزی درباره‌ی این سوراخ مهم است.
  4. (بنگ! تکه‌ی آلومینیوم را با ظرافت روی میز من می‌گذارند.)
  5. (متوجه می‌شوند که هیچ چیز مهمی درباره‌ی این سوراخ نیست، من فقط یک اندازه‌ی عجیب انتخاب کرده‌ام، و همه‌ی این کار و نگرانی اضافه بی‌دلیل بوده.)
  6. بابا، این رایان، واقعاً آدم عجیبی است. (غرغر، فحش، غرغر)

همه‌ی این‌ها اتفاق می‌افتد چون هر تصمیم طراحی من چیزی را به دیگرانی که به آن نگاه می‌کنند و روی آن کار می‌کنند منتقل می‌کند، چه بخواهم چه نخواهم. آن‌ها باید در آن معنا ببینند، چون تنها اطلاعاتی است که در دست دارند! پس خیلی بهتر است اگر بتوانم وقت بگذارم و اطلاعات معنادار و هدفمند در طراحی‌ام بگذارم.

Grains: یک مقدمه

حالا بیایید درباره‌ی اینکه قصد طراحی چطور می‌تواند در کد منتقل شود صحبت کنیم، با مثالی از یکی از تمرین‌های Exercism. اخیراً با یک دانش‌آموز روی راه‌حلش برای تمرین Grains در مسیر Bash کار کردم. Grains تمرینی است که به مسئله‌ی گندم و صفحه‌ی شطرنج می‌پردازد. به‌طور خلاصه، یک دانه‌ی گندم روی خانه‌ی اول یک صفحه‌ی شطرنج گذاشته می‌شود. دو دانه روی خانه‌ی بعدی. چهار دانه روی خانه‌ی بعدی. و همین‌طور ادامه می‌یابد، به‌طوری‌که هر خانه دو برابر خانه‌ی قبلی دانه دارد. از دانش‌آموزان خواسته می‌شود راهی پیدا کنند تا مقدار هر خانه‌ی مشخص و همچنین مجموع کل دانه‌های روی صفحه را محاسبه کنند.

این دانش‌آموز به‌خصوص، راه‌حل نسبتاً زیرکانه‌ای برای محاسبه‌ی مجموع پیدا کرد.

bc <<< 'ibase=16;FFFFFFFFFFFFFFFF'

bc یک ماشین‌حساب خط فرمان است. می‌توانید رشته‌هایی از حساب به آن بدهید و آن‌ها را محاسبه می‌کند، حتی برای اعداد صحیح بسیار بزرگ و اعداد اعشاری. راه‌های دیگری هم برای انجام محاسبات بدون استفاده از bc در Bash وجود دارد، اما به خاطر سادگی، می‌خواهیم ببینیم قصد، هنگام استفاده از bc، چطور می‌تواند منتقل شود، یا نشود.

این راه‌حل کار می‌کند چون کل تمرین حول توان‌های دو می‌چرخد، و هر جا توان‌های دو باشد، دودویی هست، و هر جا دودویی باشد، مبنای شانزده هم هست2!

راه‌حل زیرکانه‌ای است، اما این کد چه چیزی به ما می‌گوید؟ اینکه مبنای شانزده اینجا مهم است؟ اینکه مسئله اساساً حول ۱۶ می‌چرخد؟ بعد از دوباره خواندن صورت مسئله، کاملاً روشن است که هیچ‌کدام از این‌ها درست نیست. دانش‌آموز و من برای اینکه قصد را شفاف‌تر منتقل کنیم، طوفان فکری کردیم. این‌ها چند موردی است که به آن رسیدیم:

گزینه‌ی اول: دودویی

چون کلی چیز داریم که دو برابر می‌شوند (و بنابراین کلی توان ۲)، بیایید ببینیم در دودویی چه اتفاقی می‌افتد تا ببینیم این کمکی می‌کند یا نه.


خانه‌ی اول ۱ دانه دارد. در دودویی، این هم 0b1 خواهد بود (0b فقط یعنی «این یک عدد دودویی است»، و عدد واقعی همان 1 است).

خانه‌ی دوم ۲ دانه دارد. در دودویی، 0b10. مجموع تا اینجا ۳ است (یا 0b11).

خانه‌ی سوم ۴ دانه (0b100) دارد. مجموع تا اینجا: ۷ (0b111).

خانه‌ی چهارم ۸ دانه (0b1000) دارد. مجموع تا اینجا: ۱۵ (0b1111).


الگو را می‌بینید؟

هر خانه یک رقم دودویی دیگر را نشان می‌دهد، و جمع کردن همه‌شان با هم فقط یک مشت ۱ می‌سازد.

در راه‌حل دانش‌آموز، می‌توانستیم Fها را با ۶۴ تا ۱ عوض کنیم (یکی برای هر خانه)!

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 |32 |
|   | 3 | 7 |15 |31 |63 |
-------------------------

و مجموع: ۱ + ۲ + ۴ + ۸ + ۱۶ + ۳۲ = ۶۳. هوم... راستش دارم کم‌کم شروع به دیدن الگو می‌کنم، اما برای اطمینان یکی دیگر حساب می‌کنیم.

۷ خانه:

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

۱ + ۲ + ۴ + ۸ + ۱۶ + ۳۲ + ۶۴ = ۱۲۷. می‌بینید؟ آیا با مقادیر ۳۱، ۶۳، ۱۲۷ چیزی زنگ هشدار به صدا درمی‌آید؟

آن‌ها تقریباً توان‌های دو هستند. در واقع، یکی کمتر از توان بعدی دو هستند.

یک مثال دیگر، تا کاملاً جا بیفتد. یک صفحه‌ی شطرنج ۱۲ خانه‌ای را تصور کنید. یعنی یک، ۱۱ بار دو برابر شده (که در دنیای ریاضی می‌شود ۲ به توان ۱۱): ۲۰۴۸. دوباره دو برابرش کنید، می‌شود ۴۰۹۶ (۲ به توان ۱۲). پس... اگر الگو را درست فهمیده باشیم، مجموع تجمعی یکی کمتر از ۴۰۹۶ خواهد بود، یعنی همان ۴۰۹۵. و اگر حساب کنیم، دقیقاً همین را به دست می‌آوریم: ۱ + ۲ + ۴ + ۸ + ۱۶ + ۳۲ + ۶۴ + ۱۲۸ + ۲۵۶ + ۵۱۲ + ۱۰۲۴ + ۲۰۴۸ = ۴۰۹۵.

به بیان دیگر، برای یافتن مجموع همه‌ی n خانه، باید یک توان دو بالاتر بروید و ۱ را از نتیجه کم کنید.

تعداد دانه‌های روی خانه‌ی ۶۴ برابر است با ۲ به توان ۶۳ (یادتان باشد، نمایه‌گذاری از صفر است). پس اگر بخواهیم مجموع دانه‌های همه‌ی خانه‌ها تا و از جمله خانه‌ی ۶۴ را محاسبه کنیم، باید ۲ به توان ۶۴ را حساب کنیم و ۱ کم کنیم.

و تمام!

در Bash، این‌طور خواهد بود:

bc <<< "2^64 - 1"

این وقتی منطقی است که تأیید کنید در دودویی چه اتفاقی می‌افتد. در دودویی، مجموع همه‌ی ۶۴ خانه چه بود؟

0b1111...  # 64 ones

تعداد دانه‌های روی خانه‌ی فرضی ۶۵ چند است؟

0b10000... # 1 and 64 zeros

چطور از یک و ۶۴ صفر به ۶۴ تا ۱ می‌رسید؟ ۱ را کم می‌کنید.

و چه سود اضافه‌ای این به ما می‌دهد؟ خب، حالا یک عبارت مرتب و خوانا برای مجموع داریم. حلقه نمی‌زند، پس کارایی خوب است. و عدد ۶۴ را در خود دارد، که همان تعداد خانه‌های یک صفحه‌ی شطرنج است، و نمونه‌ی خوبی از قصد طراحیِ به‌خوبی نشانه‌گذاری‌شده است. اگر به دلیلی، ۱۰۰۰ سال بعد، جهان بر یک صفحه‌ی شطرنج ۷×۷ توافق کند، آن مهندس آینده (که احتمالاً از Bash 6.1 استفاده می‌کند) اسکریپت را بررسی می‌کند، می‌بیند در پی چه بودید، و ۶۴ را به ۴۹ تغییر می‌دهد. همه‌چیز خوب!

هدفمند بمانید، دوستان

وقتی سراغ پیاده‌سازی می‌روید، آسان است که چیزها را این طرف و آن طرف بیندازید و به اولین راه‌حلی بچسبید که کار می‌کند. این در حین کاوش مسئله اشکالی ندارد، اما وقتی اجزای حیاتی را کاملاً فهمیدید، اگر وقت دارید که صرف کنید و همه‌چیز را خوب صیقل دهید، مطمئن شوید که هر الگوریتم، هر اسم متغیر، و حتی فاصله‌گذاری‌تان تصویری از مسئله، نیازهای حیاتی، و اینکه چگونه همه‌ی قطعات کنار هم جا می‌گیرند رسم می‌کند.

  1. کمیک Thom Holwerda را هم ببینید. ↩

  2. اگر در شمارش دودویی و مبنای شانزده کمی زنگ‌زده‌اید، @kytrinyx کتاب How to Count را پیشنهاد می‌کند. به‌عنوان یک خودتبلیغی بی‌خجالت، من هم اخیراً چند پست وبلاگ درباره‌ی دودویی و مبنای شانزده نوشتم. ↩

  3. نمی‌دانم این چطور می‌شد. شاید می‌توانستیم فقط بگذاریم سربازها با نیزه به جان هم بیفتند. ↩

14 فوریه 2019 · برایتان مفید بود؟