درس ۴۶ از ۴۶ · مرجع

بهترین شیوه‌ها

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

۱. نخست کتابخانه‌ی استاندارد

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

تابع پایتونی را در نظر بگیرید که متن شانزده‌دهی را به عدد برمی‌گرداند:

def hex_to_decimal(hex):
    return int(hex, 16)

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

bp-std-first.salam
// زبان: فارسی
واردسازی تبدیل

تابع ریشه:
    سرچاپ تبدیل.تجزیه شانزده با پیش فرض("FF"، ۰ - ۱)، تبدیل.تجزیه شانزده با پیش فرض("0x10"، ۰ - ۱)
    سرچاپ تبدیل.تجزیه شانزده با پیش فرض("12zz"، ۰ - ۱)
    تلاش:
        سرچاپ تبدیل.تجزیه شانزده("1a2b")
        سرچاپ تبدیل.تجزیه شانزده("xyz")
    گرفتن خطا خ:
        سرچاپ "عدد شانزده‌دهی نیست:"، خ.پیام
    پایان
پایان
خروجی255 16 -1 6699 عدد شانزده‌دهی نیست: invalid character 'x'

تبدیل.تجزیه شانزده رقم‌های بزرگ و کوچک و 0x آغازین را می‌پذیرد و برای ورودی بد شکست می‌خورد، درست مثل int(s, 16) که ValueError می‌دهد. تبدیل.تجزیه شانزده با پیش فرض به جای شکست، مقداری را که خودتان گفته‌اید برمی‌گرداند. حلقه‌ی دست‌نویسی که در نخستین نویسه‌ی بد می‌ایستد، 12zz را بی‌هیچ خبری ۱۸ می‌کند.

چطور چیزی را پیدا کنیم:

  • در مرجع کتابخانه دنبال واژه‌ای بگردید که کار را توصیف کند: hex، split، sha256، base64، trim.
  • امضا و مثال صفحه‌ی تابع را بخوانید. بیشتر تابع‌ها دو شکل دارند: یکی خطادار و یکی با پیش‌فرض، با پسوند Or.
  • فقط وقتی چیزی جور نبود، خودتان بنویسید، کوچک نگه دارید و در پرونده‌ی جداگانه بگذارید تا نفر بعدی پیدایش کند.
می‌خواستید بنویسیدبه جایش بنویسید
حلقه‌ای که متن را به عدد برمی‌گرداندتبدیل.تجزیه عدد، تبدیل.تجزیه منطقی، تبدیل.تجزیه شانزده
عدد به متن با تراز یا رقم اعشارتبدیل.قالب عدد، تبدیل.قالب شانزده
شکستن، پیراستن، چسباندن و جایگزینی متنروش‌های رشته و بسته‌ی رشته
خواندن و نوشتن JSONjson
الگویابی در متنregex
چکیده، بایت تصادفی، base64crypto، encoding
مرتب‌سازی و جستجوی کلیدیsort، وکتور، نگاشت

۲. مسئله را با گونه‌ها مدل کنید

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

  • چیزی که چند فیلد وابسته دارد ساختار است. برایش متد بنویسید، سازنده را ایستا تابع کنید و فیلدها را تا وقتی بیرون به آن‌ها نیاز ندارد خصوصی نگه دارید.
  • گزینش از فهرست ثابت (حالت، نوع، فرمان) جداشمار است و با همخوان بازش می‌کنید که مترجم بررسی کند همه‌ی حالت‌ها پوشش داده شده‌اند. عدد یا متن جادویی دست‌به‌دست نکنید.
  • رفتاری که چند گونه در آن شریک‌اند میانجی است. وقتی گونه هنگام ترجمه معلوم است قید <ت: شکل> بگذارید و برای گردایه‌ی گونه‌های آمیخته پویا شکل.
  • کدی که به گونه کاری ندارد عام است: بزرگتر_از<T> را یک‌بار بنویسید، نه یک نسخه برای هر گونه.
bp-interface.salam
// زبان: فارسی
میانجی شکل:
    تابع مساحت(): اعشار۶۴
پایان

ساختار مربع:
    همگانی ضلع: اعشار۶۴ = ۰.۰
    همگانی تابع مساحت(): اعشار۶۴: برگشت این.ضلع * این.ضلع پایان
پایان

ساختار دایره:
    همگانی شعاع: اعشار۶۴ = ۰.۰
    همگانی تابع مساحت(): اعشار۶۴: برگشت ۳.۰ * این.شعاع * این.شعاع پایان
پایان

تابع بیشینه<T>(الف: T، ب: T): T:
    برگشت الف بزرگتر ب ? الف : ب
پایان

تابع مجموع(شکل ها: وکتور<پویا شکل>): اعشار۶۴:
    ناپایا جمع := ۰.۰
    هر ش در شکل ها:
        جمع += ش.مساحت()
    پایان
    برگشت جمع
پایان

تابع ریشه:
    سرچاپ بیشینه(۳، ۸)، بیشینه("گلابی"، "الف")
    شکل ها := وکتور {} برگردان وکتور<پویا شکل>
    شکل ها.بیفزا(مربع { ضلع = ۲.۰ })
    شکل ها.بیفزا(دایره { شعاع = ۱.۰ })
    سرچاپ مجموع(شکل ها)
    هر ش در شکل ها:
        ش.آزادکن()
    پایان
    شکل ها.آزادکن()
پایان
خروجی8 گلابی 7

شکست هم در گونه‌ی تابع جا دارد. تابعی که می‌تواند شکست بخورد خطادار نشانه می‌خورد و با پرتاب شکست می‌خورد؛ فراخواننده یا با تلاش خطا را بالا می‌فرستد یا در بلوک تلاش: و گرفتن آن را رسیدگی می‌کند. برای «نشد» مقدار -۱ یا متن تهی برنگردانید: فراخواننده فراموش می‌کند بررسی کند. پاک‌سازی را با دیرکن کنار چیزی بگذارید که پاک می‌کند و برای عددهای معنادار نام پایا بگذارید.

۳. تابع‌های کوچک

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

این برنامه رنگی مثل #ff8800 را به مقدار تبدیل می‌کند. همه‌ی تابع‌ها کوتاه‌اند، جفت کار شانزده‌دهی را به تبدیل می‌سپارد، شکست یک پرتاب است، مولفه جداشمار است و ریشه فقط چاپ می‌کند:

bp-design.salam
// زبان: فارسی
واردسازی تبدیل

جداشمار مولفه: قرمز، سبز، آبی پایان

ساختار رنگ:
    همگانی مقدار قرمز: صحیح = ۰
    همگانی مقدار سبز: صحیح = ۰
    همگانی مقدار آبی: صحیح = ۰

    همگانی تابع مقدار(م: مولفه): صحیح:
        برگشت همخوان م:
            قرمز: این.مقدار قرمز پایان
            سبز: این.مقدار سبز پایان
            آبی: این.مقدار آبی پایان
        پایان
    پایان

    همگانی تابع به رشته(): رشته:
        برگشت "rgb(" + این.مقدار قرمز + ", " + این.مقدار سبز + ", " + این.مقدار آبی + ")"
    پایان
پایان

تابع جفت(متن: رشته، از: صحیح): صحیح خطادار:
    برگشت (تلاش تبدیل.تجزیه شانزده(متن.زیررشته(از، ۲))) برگردان صحیح
پایان

تابع خواندن رنگ(متن: رشته): رنگ خطادار:
    ارقام := متن.شروع با("#") ? متن.زیررشته(۱، متن.طول() - ۱) : متن
    اگر ارقام.طول() نابرابر ۶:
        پرتاب خطا خطا.جدید(نوع خطا.آرگومان نامعتبر، "شش رقم شانزده‌دهی لازم است، نه " + ارقام)
    پایان
    برگشت رنگ { مقدار قرمز = تلاش جفت(ارقام، ۰)، مقدار سبز = تلاش جفت(ارقام، ۲)، مقدار آبی = تلاش جفت(ارقام، ۴) }
پایان

تابع مولفه پررنگ(ر: رنگ): مولفه:
    ناپایا بهترین := مولفه.قرمز
    هر م در [مولفه.سبز، مولفه.آبی]:
        اگر ر.مقدار(م) بزرگتر ر.مقدار(بهترین):
            بهترین = م
        پایان
    پایان
    برگشت بهترین
پایان

تابع توضیح(متن: رشته): رشته:
    تلاش:
        ر := تلاش خواندن رنگ(متن)
        برگشت ر.به رشته() + "، پررنگ‌ترین: " + مولفه پررنگ(ر).نام()
    گرفتن خطا خ:
        برگشت "رنگ نامعتبر: " + خ.پیام
    پایان
پایان

تابع ریشه:
    سرچاپ توضیح("#ff8800")
    سرچاپ توضیح("0a141e")
    سرچاپ توضیح("#12")
    سرچاپ توضیح("#gg0000")
پایان
خروجیrgb(255, 136, 0)، پررنگ‌ترین: قرمز rgb(10, 20, 30)، پررنگ‌ترین: آبی رنگ نامعتبر: شش رقم شانزده‌دهی لازم است، نه 12 رنگ نامعتبر: invalid character 'g'

۴. به پرونده‌ها بشکنیدش

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

colourtool/
  main.salam         reads the command line, calls the rest
  salam.toml         name, version, dependencies
  model.salam        Rgb, Channel
  parse.salam        parse_colour and its helpers
  parse_test.salam   test_ functions for parse.salam

درس بسته‌ها و پرونده‌ها نشان می‌دهد پرونده‌ها چطور همدیگر را فرا می‌خوانند و پوشه چطور بسته می‌شود. فرمان salam new name چیدمان آغازین را می‌سازد. آزمون‌ها کنار کد در پرونده‌هایی با پایان _test.salam می‌نشینند و با salam test اجرا می‌شوند.

۵. سلام بنویسید، نه برگردان لفظی

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

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

۶. نام‌ها را به زبان پرونده بگذارید

هر پرونده‌ی سلام به یک زبان نوشته می‌شود و نام‌هایش هم باید همان زبان باشند. در برنامه‌ی فارسی برای تابع‌ها، متغیرها، پارامترها، فیلدهای ساختار، عضوهای جداشمار و ثابت‌ها نام فارسی بگذارید؛ در برنامه‌ی انگلیسی نام انگلیسی. این دو را در یک پرونده یا یک نام با هم نیامیزید و واژه‌ی فارسی را با حرف لاتین ننویسید (rang، khandan). حرف‌های تکی مثل i و x، سرنام‌های شناخته‌شده مثل URL و JSON، نام تابع‌های test_ و نام تابع‌های C در بلوک extern: در هر دو زبان رواست. متنی که به کاربر نشان داده می‌شود به مخاطبتان بستگی دارد، نه به زبان پرونده.

نوشتن نامِ چندواژه‌ای به زبان بستگی دارد. در انگلیسی نام هرگز فاصله ندارد: برای تابع و متغیر و پارامتر و فیلد از snake_case و برای گونه‌ها و رابط همگانی از PascalCase استفاده کنید، همان‌طور که کتابخانه‌ی استاندارد می‌کند (parse_colour، red_value، Rgb). در فارسی استاندارد این است که واژه‌ها را با فاصله از هم جدا کنید و مترجم آن را برای تابع، متد، پارامتر، فیلد ساختار، عضو جداشمار و متغیر می‌پذیرد، مثل خواندن رنگ یا جمع کل. در نام‌های فارسی زیرخط نگذارید. دستور زبان برای نام ساختار و جداشمار و میانجی، برای ثابت، برای متغیر حلقه، برای متغیر گرفتن و برای پارامتر تابع بی‌نام و پارامتر درون‌داد: فقط یک واژه می‌پذیرد؛ آنجا واژه‌ها را با نیم‌فاصله بچسبانید (رنگ‌آمیزی) که مترجم آن را دقیقا مثل فاصله می‌داند. و هیچ واژه‌ی نامِ چندواژه‌ای نباید واژه‌ی کلیدی باشد: بیشینه از نام خوبی است ولی بزرگتر از نه، چون بزرگتر یعنی gt.

کتابخانه‌ی استاندارد برای همه‌چیز نام فارسی دارد، پس برنامه‌ی فارسی تبدیل.تجزیه شانزده و .طول() را صدا می‌زند. فرمان salam translate واژه‌های کلیدی و نام کتابخانه را برمی‌گرداند ولی نام‌هایی را که خودتان ساخته‌اید نه؛ پس بعد از ترجمه‌ی یک پرونده آن‌ها را با دست عوض کنید. مثال‌های همین درس این کار را کرده‌اند: خواندن رنگ همتای فارسی parse_colour در صفحه‌ی انگلیسی است.

۷. کارتان را بسنجید

این‌ها را بعد از هر تغییر اجرا کنید. سریع‌اند و مترجم عمدا سخت‌گیر است (قواعد مترجم را ببینید):

  • salam check app.salam بی‌ساختن برنامه خطاها را پیدا می‌کند؛
  • salam format app.salam کد را به چیدمان استاندارد درمی‌آورد؛
  • salam test تابع‌های test_ را اجرا می‌کند؛
  • salam run app.salam برنامه را می‌سازد و اجرا می‌کند.

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