گونهبندی ایستا (Static Typing) یعنی گونهی هر مقدار هنگام کامپایل برنامه معلوم است و بررسی میشود، نه آنکه هنگام اجرا کشف شود. زبان برنامهنویسی سلام گونهبندی ایستا دارد: هر متغیر، پارامتر، فیلد و عبارت یک گونه دارد، کامپایلر هر استفاده را با آن میسنجد و برنامهای که خطای گونه داشته باشد ساخته نمیشود. بهندرت لازم است گونهی متغیرهای محلی را بنویسید، چون کامپایلر آن را از مقدار اولیه استنتاج میکند، اما گونهی پارامترها و خروجی تابعها را همیشه مینویسید. این مقاله مرور میکند سلام چه چیزهایی را بررسی میکند، با همان پیامهایی که کامپایلر برای کد فارسی چاپ میکند، و بررسیهایش کجا متوقف میشوند.
زبان با گونهبندی ایستا چه سودی دارد
در یک زبان با گونهبندی پویا مانند پایتون یا جاوااسکریپت، قیمت * تعداد وقتی بررسی میشود که همان سطر اجرا شود: اگر تعداد آن روز از قضا یک رشته باشد، در محیط عملیاتی متوجه میشوید. در یک زبان با گونهبندی ایستا مانند Go، Rust، جاوا یا سلام، همان اشتباه یک خطای کامپایل است که روی دستگاه خودتان و پیش از آنکه برنامه حتی شروع شود پیدا میشود.
معاوضهها شناختهشدهاند. گونههای ایستا دستهی کاملی از باگها را زود میگیرند، گشتن در کد را آسانتر میکنند (ویرایشگر میتواند دقیقاً بگوید کاربر.نشانی چیست) و به کامپایلر اجازه میدهند کد سریعتری بسازد، چون لازم نیست گونهها را در زمان اجرا بررسی کند. بهایشان کمی انعطاف است و، در زبانهای قدیمیتر، تایپ کردن زیاد. زبانهای ایستای امروزی، از جمله سلام، بیشتر آن راحتی را با استنتاج گونه (Type Inference) بازمیگردانند.
سلام بررسیکنندهی گونه را پایهای برای بررسیهایی بیش از خود گونهها هم قرار داده است. پیش از تولید هر کدی، تحلیل معنایی کامپایلر (که در مقالهی مراحل کامپایلر شرح داده شده) گونهها، تغییرپذیری، کامل بودن، مقداردهی قطعی، کد بیاستفاده، بازهی مقدارها و مالکیت حافظه را بررسی میکند.
استنتاج گونه برای متغیرهای محلی در زبان برنامهنویسی سلام
استنتاج گونه یعنی کامپایلر گونه را از روی کد پیدا میکند، به جای آنکه شما آن را بنویسید. در سلام، := یک متغیر تعریف میکند و گونهاش از مقدار میآید:
تابع میانگین(مقدارها: اعشار۶۴[]): اعشار۶۴:
ناپایا جمع := ۰.۰
هر م در مقدارها:
جمع += م
پایان
برگشت جمع / طول(مقدارها) برگردان اعشار۶۴
پایان
تابع ریشه:
شمار := ۳
نسبت := ۰.۷۵
بزرگ := ۵۰۰۰۰۰۰۰۰۰ برگردان صحیح۶۴
نام := "سلام"
کافی := شمار > ۲
سرچاپ شمار، نسبت، بزرگ، نام، کافی
سرچاپ میانگین([۲.۰، ۴.۰، ۹.۰])
پایانsalam inspect --emit-ast نشان میدهد کامپایلر چه استنتاج کرده است: شمار از گونهی i32 است، نسبت از f64، بزرگ از i64، نام از str و کافی از bool، و درون میانگین متغیر حلقهی م از گونهی f64 است. چند قاعده ارزش دانستن دارد:
- لیترال عدد صحیح از گونهی
صحیحاست که در سلام همانi32سیودوبیتی است (نه ۶۴ بیتی، آنطور که Go روی بیشتر سکوها دارد). وقتی مقداری جای بیشتری لازم دارد ازبرگردان صحیح۶۴استفاده کنید. - لیترالی که ممیز دارد از گونهی
اعشار۶۴است. ممیز در کد همیشه نقطه است (۲.۵)؛ ممیز فارسی٫در کد پذیرفته نمیشود. برگردان(در انگلیسیas) تنها راه بیان گونه برای یک متغیر محلی است:بزرگ := ۵۰۰۰۰۰۰۰۰۰ برگردان صحیح۶۴،و۱ := وکتور {} برگردان وکتور<رشته>.- گونهی پارامترها و خروجی تابع همیشه نوشته میشود. استنتاج محلی است، پس امضای هر تابع هم مستند آن است و هم قراردادش.
نوشتن گونه برای یک متغیر محلی، آنطور که در بسیاری از زبانهای دیگر رایج است، مجاز نیست:
خطا: اعلان با گونه صریح حذف شده است؛ از 'نام := مقدار' (یا 'نام := مقدار برگردان گونه') استفاده کنید (نزدیک '=')این نحو (ایکس: صحیح = ۵) در نسخههای قدیمیتر وجود داشت و در ۲۰۲۶ حذف شد؛ یکی از تغییرهایی که لازمهی زبانی پیش از نسخهی ۱٫۰ است. درس گونهها و درس متغیرها مقدمات را پوشش میدهند.
تبدیلهای سختگیرانه: هیچ باریکسازی بیصدایی در کار نیست
سلام مقداری را فقط وقتی بهطور ضمنی تبدیل میکند که هیچ چیز از دست نرود. یک صحیح که در حساب کنار یک اعشار۶۴ بیاید پهن میشود، پس ۳.۵ + ۲ میشود 5.5. هر چیزی که ممکن است اطلاعات از دست بدهد یک برگردان صریح لازم دارد:
تابع نصف(ن: صحیح۳۲): صحیح۳۲:
برگشت ن / ۲
پایان
تابع ریشه:
بزرگ := ۳۰۰۰۰۰۰۰۰۰ برگردان صحیح۶۴
سرچاپ نصف(بزرگ)
پایانخطا[خطا۰۱۲]: آرگومان ۱: نمیتوان 'صحیح۶۴' را به 'صحیح' پاس داد
--> n3.salam:7:11
|
7 | سرچاپ نصف(بزرگ)
| ^
|شرطها باید واقعاً منطقی (بولی) باشند. C و جاوااسکریپت اجازه میدهند یک عدد صحیح جای درست یا نادرست بنشیند؛ سلام اجازه نمیدهد:
خطا[خطا۰۲۱]: شرط 'اگر' باید بولی باشد، اما 'صحیح' دریافت شد
--> n4.salam:3:9
|
3 | اگر آماده:
| ^
|سختگیری به چیزهایی هم میرسد که در بیشتر زبانها خطای گونه نیستند. متغیر، پارامتر، واردسازی یا تابع بیاستفاده خطاست. شاخهای که هرگز اجرا نمیشود خطاست. تبدیلی که هیچ چیز را عوض نمیکند ([۱، ۲، ۳] برگردان صحیح[۳]) خطای E093 است. مقداردهی دوباره به متغیری که ناپایا تعریف نشده خطای E013 است. درس قاعدهها فهرست اینها را آورده است؛ ایده این است که کد خوانا کدی است که هیچ چیزش تصادفی نیست.
برای مقدارهایی که بازنمایی یکسان دارند اما معنای یکسان ندارند، گونه متر: صحیح یک گونهی متمایز میسازد: در حساب مثل صحیح رفتار میکند اما بدون تبدیل صریح هرگز با یک صحیح معمولی قاطی نمیشود. در مقابل، گونه سن = صحیح فقط یک نام مستعار است. درس گونههای تازه هر دو را نشان میدهد.
جنریکها و میانجیها
جنریکها (تابعها و گونههای عام) اجازه میدهند یک تابع یا گونه روی گونههای گوناگون کار کند و همچنان کاملاً بررسی شود. یک تابع عام در سلام پارامترهای گونهای خود را درون < > نام میبرد و کامپایلر آنها را از آرگومانها استنتاج میکند. یک پارامتر گونهای میتواند با یک میانجی (Interface) محدود شود، و میانجیها ساختاری برآورده میشوند: ساختاری که متدهای همگانی درست را داشته باشد آن میانجی را پیادهسازی میکند، بیآنکه اعلامش کند.
میانجی شکل:
تابع مساحت(): اعشار۶۴
تابع نام(): رشته
پایان
ساختار دایره:
همگانی شعاع: اعشار۶۴
همگانی تابع مساحت(): اعشار۶۴:
برگشت ۳.۱۴۱۵۹ * این.شعاع * این.شعاع
پایان
همگانی تابع نام(): رشته:
برگشت "دایره"
پایان
پایان
ساختار مربع:
همگانی ضلع: اعشار۶۴
همگانی تابع مساحت(): اعشار۶۴:
برگشت این.ضلع * این.ضلع
پایان
همگانی تابع نام(): رشته:
برگشت "مربع"
پایان
پایان
تابع بیشتر<ت>(الف: ت، ب: ت): ت:
اگر الف > ب:
برگشت الف
پایان
برگشت ب
پایان
تابع گزارش<ت: شکل>(ش: ت):
سرچاپ "مساحت"، ش.نام() + ":"، ش.مساحت()
پایان
تابع ریشه:
سرچاپ بیشتر(۳، ۹)، بیشتر(۲.۵، ۱.۵)، بیشتر("سیب"، "گلابی")
گزارش(دایره { شعاع = ۱.۰ })
گزارش(مربع { ضلع = ۲.۰ })
شکلها := [دایره { شعاع = ۲.۰ }، مربع { ضلع = ۳.۰ }] برگردان پویا شکل[۲]
ناپایا مجموع := ۰.۰
هر ش در شکلها:
مجموع += ش.مساحت()
پایان
سرچاپ "مجموع مساحتها:"، مجموع
پایانجنریکهای محدودی مانند گزارش<ت: شکل> تکریختسازی (Monomorphization) میشوند: کامپایلر برای هر گونهای که به کار رفته نسخهی جداگانهای میسازد، همانطور که قالبهای C++ و جنریکهای Rust میکنند، پس هزینهای در زمان اجرا ندارند. وقتی مجموعهای ناهمگون لازم دارید، پویا شکل (در انگلیسی dyn Shape) فراخوانی پویا را از راه جدولی از متدها فراهم میکند، مثل آرایهی بالا. دو آرگومان بیشتر باید همگونه باشند: بیشتر(۲، "ایکس") با پیام آرگومان ۲: نمیتوان 'رشته' را به 'صحیح' پاس داد شکست میخورد، و گزارش(۴۲) با این پیام (E002):
خطا[خطا۰۰۲]: 'صحیح' شرط 'شکل' را برآورده نمیکند: هیچ تعریف ساختار یا بلوک `کاربست` متدهای آن را فراهم نمیکنددرس تابعهای عام جلوتر میرود.
جداشمار دادهدار و همخوان کامل
جداشمار دادهدار، که گونهی جمعی (Sum Type) یا اجتماع برچسبدار هم نامیده میشود، گونهای است که مقدارش دقیقاً یکی از چند حالت نامدار است و هر حالت میتواند فیلدهای خودش را داشته باشد. Rust، Swift، Kotlin (از راه کلاسهای sealed) و خانوادهی ML این را دارند؛ Go و C ندارند. در سلام اینها همان جداشمارهای معمولیاند که عضوهایشان پارامتر میگیرند، و همخوان (در انگلیسی match) آنها را باز میکند:
جداشمار پرداخت:
نقد
کارت(شماره: رشته)
حواله(بانک: رشته، پیگیری: صحیح)
پایان
تابع شرح(پ: پرداخت): رشته:
برگشت همخوان پ:
نقد: "پرداخت نقدی" پایان
کارت(شماره): "کارتی که به " + شماره + " ختم میشود" پایان
حواله(بانک، پیگیری): "حواله از بانک " + بانک + "، پیگیری " + پیگیری پایان
پایان
پایان
تابع ریشه:
پرداختها := [پرداخت.نقد، پرداخت.کارت("4242")، پرداخت.حواله("ملی"، ۹۰۸۱)]
هر پ در پرداختها:
سرچاپ شرح(پ)
پایان
پایاننکتهی مهم این است که همخوان باید کامل باشد. اگر بعدها روش پرداخت چهارمی بیفزایید، هر همخوانی که آن را فراموش کرده دیگر کامپایل نمیشود. این اتفاقی است که وقتی یک جداشمار شکل عضو خالی پیدا میکند و یک تابع آن را پوشش نمیدهد میافتد:
خطا[خطا۰۷۹]: همخوانی ناقص روی جداشمار 'شکل': عضو(های) شکل.خالی پوشش داده نشدهاند (یا یک شاخهی 'وگرنه' بیفزایید)
--> m1.salam:8:11
|
8 | برگشت همخوان ش:
| ^
|این سودمندترین بررسیای است که یک سامانهی گونه میتواند به یک پایگاه کد رو به رشد بدهد: کامپایلر هر جایی را که حالت تازه باید آنجا رسیدگی شود پیدا میکند. درس جداشمارها و درس همخوان شکلهای دیگر را، از جمله نگهبانها و الگوهای ساختار، نشان میدهند.
مقدار اختیاری با T?
تونی هور (Tony Hoare) ارجاع تهی (null) را اشتباه میلیارد دلاری خود نامید. پاسخ سلام برای «این مقدار ممکن است نباشد» گونهی اختیاری T? است، کوتاهشدهی Option<T> از بستهی core (با نام فارسی اختیاری). فیلدی از گونهی رشته? را میتوان در لیترال ساختار ننوشت، مقدارهای ساده خودکار بستهبندی میشوند، و مقدار را با دارد()، یابرگردان() و همخانوادههایشان (در انگلیسی is_some() و unwrap_or()) میخوانید:
ساختار کاربر:
همگانی نام: رشته
همگانی لقب: رشته?
همگانی سن: صحیح?
پایان
تابع خوشامد(ک: کاربر): رشته:
اگر ک.لقب.دارد():
برگشت "سلام " + ک.لقب.مقدار
پایان
برگشت "درود بر " + ک.نام
پایان
تابع ریشه:
الف := کاربر { نام = "سارا احمدی"، لقب = "ساری" }
ب := کاربر { نام = "رضا کریمی"، سن = ۳۱ }
سرچاپ خوشامد(الف)
سرچاپ خوشامد(ب)
سرچاپ ب.سن.یابرگردان(۰)، الف.سن.یابرگردان(۰)
پایاناین گونه نبودنِ مقدار را در هر امضا آشکار میکند. البته به محکمی Option در Rust نیست: خواندن .مقدار بدون بررسی دارد() خطای کامپایل نیست، و روی یک رشته? خالی یک رشتهی تهی میدهد. هر جا پیشفرض معقولی هست، یابرگردان را ترجیح دهید.
خطا بهعنوان مقدار، و پرتاب بررسیشده
سلام دو راه میدهد تا شکست بخشی از گونهی یک تابع باشد.
راه نخست یک جداشمار است: نتیجه<ت، خ> را با عضوهای Ok و Err تعریف کنید؛ عملگر پسوندی ? یک Ok را باز میکند یا Err را زودتر برمیگرداند، مانند Rust. عملگر ? عضوهایی با همین دو نام را میجوید، پس این دو عضو حتی در کد فارسی نام انگلیسی خود را نگه میدارند:
جداشمار نتیجه<ت، خ>:
Ok(مقدار: ت)
Err(خطا: خ)
پایان
تابع رقم از(م: رشته): نتیجه<صحیح، رشته>:
اگر م.شمارنویسه() != ۱ یا وارونه "0123456789۰۱۲۳۴۵۶۷۸۹".دربردارد(م):
برگشت نتیجه.Err("رقم نیست: " + م)
پایان
برگشت نتیجه.Ok(م.به صحیح())
پایان
تابع جمع رقمها(الف: رشته، ب: رشته): نتیجه<صحیح، رشته>:
ایکس := رقم از(الف)?
ایگرگ := رقم از(ب)?
برگشت نتیجه.Ok(ایکس + ایگرگ)
پایان
تابع نمایش(ن: نتیجه<صحیح، رشته>):
همخوان ن:
Ok(م): سرچاپ "جمع:"، م پایان
Err(خ): سرچاپ "خطا:"، خ پایان
پایان
پایان
تابع ریشه:
نمایش(جمع رقمها("۴"، "5"))
نمایش(جمع رقمها("۴"، "ب"))
پایانهمانطور که میبینید، "۴" با رقم فارسی و "5" با رقم لاتین هر دو با .به صحیح() به عدد تبدیل میشوند.
راه دوم خطادار است (در انگلیسی throws). تابعی که خطادار تعریف شود میتواند یک خطا (Error) را پرتاب خطا کند، و کامپایلر میخواهد هر فراخوانی آن علامت داشته باشد: تلاش خطا را به بالا میفرستد، تلاش ... گرفتن خطا مقدار یک مقدار جایگزین میدهد و یک بلوک تلاش: آن را رسیدگی میکند. هیچ چیز پشت سر شما باز نمیشود و دیرکن همچنان اجرا میشود. فراموش کردن علامت خطای کامپایل است، و همین آن را از استثناهای جاوا، C# یا پایتون جدا میکند:
خطا[خطا۱۵۵]: این فراخوانی میتواند خطا پرتاب کند: آن را با 'تلاش' علامت بزنید تا خطا به بالا برسد، یا درون یک بلوک 'تلاش:' بگذارید تا آن را مدیریت کنید
--> th2.salam:9:11
|
9 | سرچاپ درگاه از("80")
| ^
|فراتر از گونه: بررسی بازه، سرریز و حافظه
گونه به شما میگوید یک مقدار صحیح است؛ نمیگوید آن صحیح اندیس معتبری هست یا نه. از نوامبر ۲۰۲۶ کامپایلر سلام روی هر تابع یک تحلیل بازهی مقدار اجرا میکند (compiler/semantic/sema_range.salam، طراحی در docs/design/RANGES.md). این تحلیل بازهی مقدار متغیرهای محلی و پارامترها، طول ظرفها و تهی بودن یا نبودن اشارهگرها را دنبال میکند، و حتی حلقههای کوتاه را دور به دور میپیماید. خطای یکیکمیکیزیاد (Off-by-one) که هر برنامهنویسی دستکم یک بار مینویسد:
خطا[خطا۱۴۶]: اندیس خارج از محدوده است: 'ای' برابر ۳ است، اما 'عددها' ۳ عضو دارد (در دور ۴ حلقهی خط ۴)
--> r1.salam:5:16
|
5 | جمع += عددها[ای]
| ^
|
= راهنما: اندیسهای معتبر 'عددها' از ۰ تا ۲ هستند; 'تکرار a تا b' خود b را هم در بر میگیرد، پس با 'تکرار xs.len() در i:' یا 'هر i, x در xs:' بشماریدیک حلقهی فاکتوریل که از گنجایش عدد صحیح ۳۲ بیتی بیرون میزند، و تقسیم بر متغیری که صفر است:
خطا[خطا۱۴۸]: سرریز عدد صحیح: 'ف *= ای' برابر ۶۲۲۷۰۲۰۸۰۰ میشود که در 'i32' جا نمیشود (در دور ۱۳ حلقهی خط ۳)
خطا[خطا۱۵۰]: تقسیم بر صفر: مقسومعلیه 'د' اینجا ۰ استخطای این گذر (E146، E148، E150، E152) یعنی آن سطر هر بار که اجرا شود اشتباه میکند. هشدار (W147 به بعد) یعنی مقداری که تحلیل میتواند ببیند آن سطر را به اشتباه میاندازد. مقدارهایی که تحلیل چیزی دربارهشان نمیداند، مانند یک پارامتر یا خروجی یک تابع، هرگز گزارش نمیشوند، پس تحلیل حدس نمیزند. --range-checks=error هشدارها را به خطا تبدیل میکند و off این گذر را خاموش میکند.
همین ایده برای حافظه هم به کار میرود: یک تحلیل مالکیت استفاده پس از آزادسازی و آزادسازی دوباره را در زمان کامپایل و بدون هیچ حاشیهنویسی گزارش میکند. مقالهی مدیریت حافظه به آن میپردازد.
سامانهی گونهی زبان برنامهنویسی سلام کجا متوقف میشود
بهتر است دقیق بگوییم این بررسیها چه کارهایی نمیکنند:
- وارسیگر قرض (Borrow Checker) ندارد. Rust در زمان کامپایل ثابت میکند ارجاعها هرگز بیشتر از دادهشان عمر نمیکنند. تحلیل مالکیت سلام بسیاری از اشتباههای رایج را میگیرد اما در حالت کلی چیزی را ثابت نمیکند، و آزاد کردن حافظهی هیپ همچنان کار شماست.
--releaseبررسیهای زمان اجرا را حذف میکند. ساخت توسعه اندیس آرایه ووکتوررا در زمان اجرا بررسی میکند و با یک panic متوقف میشود؛ ساخت انتشار این بررسیها را برای سرعت برمیدارد و فرض میکند حساب علامتدار سرریز نمیکند.- استنتاج فقط محلی است. امضای تابعها باید نوشته شود، برخلاف زبانهایی با استنتاج سراسری برنامه مانند Haskell یا OCaml.
- مقدار اختیاری را میشود ناایمن باز کرد، همانطور که بالاتر گفته شد.
اینها محدودیتهای واقعی یک زبان جوانِ پیش از نسخهی ۱٫۰ است. در همین چارچوب، کامپایلر پیش از زمان اجرا بیشتر از آنچه بیشتر زبانهای جریان اصلی بهطور پیشفرض میگیرند خطا پیدا میکند. اگر میخواهید امتحانش کنید، ویرایشگر آنلاین سلام را در مرورگر کامپایل و اجرا میکند و صفحهی نصب مجموعهابزار کامل را دارد.
پرسشهای پرتکرار
آیا زبان سلام گونهبندی ایستا دارد یا پویا؟
ایستا. هر عبارت گونهای دارد که در زمان کامپایل معلوم است و خطاهای گونه ساخت را متوقف میکنند. متغیرهای محلی معمولاً به نوشتن گونه نیاز ندارند، چون کامپایلر آن را استنتاج میکند.
آیا زبان سلام استنتاج نوع دارد؟
بله، برای متغیرهای محلی و برای آرگومانهای گونهای فراخوانیهای عام. شمار := ۳ از گونهی صحیح است، ایکس := ۰.۵ از گونهی اعشار۶۴، و در بیشتر(۳، ۹) گونهی ت همان صحیح استنتاج میشود. گونهی پارامترها و خروجی تابع همیشه صریح نوشته میشود.
آیا سلام null دارد؟
اشارهگرها میتوانند پوچ (در انگلیسی null) باشند، بیشتر برای کار با C. برای مقدارهای معمولی که ممکن است نباشند، از گونهی اختیاری T? استفاده کنید که نبودنِ احتمالی را بخشی از گونه میکند تا در هر امضا دیده شود.
فرق بررسی گونهی ایستا با بررسی بازهی سلام چیست؟
بررسی گونه ثابت میکند یک مقدار از جنس درستی است (عدد صحیح، رشته). تحلیل بازهی سلام جلوتر میرود و دنبال میکند یک متغیر چه مقدارهایی میتواند داشته باشد، تا بتواند اندیسی که همیشه بیرون از محدوده است، سرریزی که همیشه رخ میدهد یا مقسومعلیهی که همیشه صفر است را پیش از اجرای برنامه گزارش کند.