اتّحادات موسومة، بأسلوب Zero
choice يُعلن نوعًا قيمته واحدة من عدة متغيِّرات مُسمّاة، كل متغيِّر يحمل حمولته الخاصّة:
choice Result {
ok: i32,
err: String,
}
قيمة Result إمّا ok تحمل i32 أو err تحمل String. لا الاثنان معًا، ولا لا شيء. نظام الأنواع يضمن ذلك، وmatch يجعل التصرّف بناءً عليها مريحًا.
هذه هي الفكرة نفسها التي تُسمّيها لغات أخرى "اتّحاد موسوم" أو "نوع مجموع" أو "اتّحاد مميَّز" أو "نوع بيانات جبري". Zero تُسمّيها choice وتُبقي الصياغة صغيرة.
إعلان choice
choice Name {
variantA: PayloadTypeA,
variantB: PayloadTypeB,
}
كل سطر يذكر متغيِّرًا واحدًا. الاسم لك؛ والنوع بعد النقطتين هو الحمولة التي يحملها ذلك المتغيِّر. متغيِّر لا يحتاج حمولة يستخدم Void:
choice Token {
word: String,
number: i32,
eof: Void,
}
Token.eof متغيِّر بلا حمولة مفيدة (حمولته Void) — مفيد لحالات منهية كالنهاية.
بناء قيمة choice
ابنِ قيمة بتسمية النوع، ثم المتغيِّر، ثم تمرير الحمولة:
let success = Result.ok(42)
let failure = Result.err("validation failed")
نوع الحمولة يجب أن يطابق الحمولة المُعلَنة للمتغيِّر. Result.ok("hello") ستكون خطأ ترجمة لأن ok تتوقّع i32.
استنتاج النوع يعمل هنا أيضًا. إذا حدّد الجانب الأيمن النوع بالكامل، تستطيع كتابة let success = Result.ok(42) ويصبح نوع الربط Result. تعليق النوع صراحة مقبول عندما تريد توثيقه في موقع الربط:
let success: Result = Result.ok(42)
مطابقة choice
match هي الطريقة التي تقرأ بها قيمة choice. الشكل:
match value {
.variantA => binding { /* الجسم حين تكون value هي variantA، مع الحمولة في `binding` */ }
.variantB => binding { /* الجسم حين تكون value هي variantB */ }
}
مثال متكامل من مستودع Zero الرسمي — اضغط Run لرؤية فرع .ok يعمل:
اقرأ match حرفيًا: "بحسب المتغيِّر الذي يحمله result، شغّل الفرع المطابق واربط الحمولة بالاسم المختار." في فرع .ok، value هي حمولة i32. في فرع .err، message هي حمولة String. كل فرع نطاق منفصل؛ الربط مرئي داخل جسمه فقط.
الشمولية
هذه هي الفائدة الكبرى من match على سلاسل if/else if: يتحقّق المترجم من أن لكل متغيِّر فرعًا. إن نسيت حالة .err، لن تحصل على تمرير في زمن التشغيل إلى فرع افتراضي — بل ستحصل على خطأ ترجمة:
{
"code": "MAT001",
"message": "match is not exhaustive: missing variant 'err'",
"line": 9
}
(رمز الخطأ توضيحي؛ المبدأ هو العقد.)
أضف متغيِّرًا جديدًا إلى choice — مثلًا Result.timeout: Void — وكل match ضدّ Result في الشيفرة يصبح خطأ ترجمة حتى تُعالج الحالة الجديدة. هذه ميزة، لا عبء: المترجم يُخبرك بالضبط أين تحتاج الحالة الجديدة اهتمامًا.
حين لا تحتاج الحمولة
إن كانت حمولة متغيِّر Void أو ببساطة لا تهتمّ بها في هذا الفرع، تستطيع تجاهل الربط — لكن لا يزال يجب كتابة الفرع لإرضاء الشمولية:
match token {
.word => w { /* استخدم w */ }
.number => n { /* استخدم n */ }
.eof => _ { /* لا شيء للربط */ }
}
الكتابة الدقيقة لـ "تجاهل الحمولة" قد تتطوّر في Zero قبل الإصدار 1.0 (قد ترى _ أو ببساطة حذف الربط). النقطة المفاهيمية — كل متغيِّر يحصل على فرع، بحمولة أم بدون — هي الجزء المستقرّ.
أنماط شائعة
نوع خطأ بنمط Result
هذا هو المثال الذي يستخدمه المستودع الرسمي تمامًا:
choice Result {
ok: i32,
err: String,
}
الدوال التي تستطيع النجاح-بقيمة أو الفشل-برسالة تُعيد Result. يطابق المستدعون النمط لاستخراج إمّا القيمة أو الرسالة. نظام raises/check في Zero يتعامل مع النشر للعمليات القابلة للفشل؛ أمّا Result فمفيد عندما تريد الاحتفاظ بقيمة نجاح-أو-فشل كبيانات.
رمز محلِّل
choice Token {
word: String,
number: i32,
eof: Void,
}
يُنتج مُحَلِّل الرموز (tokenizer) تدفّقًا من Tokens. كل مستهلك يطابق على المتغيِّر ليقرّر ما يفعل — اطبع الكلمة، اجمع الرقم، اخرج عند eof.
آلة حالات
choice State {
waiting: Void,
processing: i32,
done: String,
}
processing تحمل مُعرِّف المهمة الحالية؛ وdone تحمل النتيجة النهائية. كل انتقال قيمة State جديدة — بلا حقول قابلة للتعديل متناثرة عبر بنية.
Choice مع التعميم
choice تستطيع أن تكون عامة (generic) تمامًا مثل shape:
choice Maybe<T> {
some: T,
none: Void,
}
Maybe<i32> هو "عدد صحيح اختياري". Maybe<String> هو "نصّ اختياري". هذا النمط نفسه يظهر في مكتبة Zero القياسية وهو ملاءمة أفضل بكثير من قيمة null حارسة — لا توجد طريقة لنسيان حالة .none بمجرّد أن تُطابق match ضدّ النوع.
متى تستخدم Choice مقابل Shape مقابل Enum
استعراض سريع من البِنى shapes والتعدادات enums:
- Shape — سجلّ بحقول متعدّدة، كلّها حاضرة معًا.
- Enum — واحدة من N تسميات، بلا بيانات إضافية.
- Choice — واحدة من N متغيِّرات، كلٌّ تحمل حمولة.
معظم نماذج البيانات في برنامج حقيقي مزيج ما من هذه الثلاثة. وضوح البدء من "هل هذا و، أم أو، أم أو ببيانات؟" هو واحدة من الفوائد التي يُقلَّل من شأنها عند العمل بلغة صغيرة.
التالي: قدرة World
choice وmatch يُغطّيان جانب البيانات في Zero. الفصل التالي يدور حول التأثيرات — كيف تتفاعل برامج Zero مع العالم الخارجي. يبدأ بـ قدرة World، الكائن الذي يحرس كل قطعة من الإدخال/الإخراج.
الأسئلة الشائعة
ما هو choice في Zero؟
choice هو نوع الاتّحاد الموسوم في Zero — قيمة تكون واحدة من عدة متغيِّرات مُسمّاة، كل متغيِّر يحمل نوع حمولته الخاصّ. مثلًا: choice Result { ok: i32, err: String }. قيمة Result إمّا ok تحمل i32 أو err تحمل String. تبنيها بـ Result.ok(42) أو Result.err("bad").
كيف تعمل match في Zero؟
match value { .variantA => binding { ...body } .variantB => binding { ...body } } يتفرّع على المتغيِّر الذي يحمله value. كل فرع يُطابق متغيِّرًا، يُسمّي ربط الحمولة، وينفّذ جسمه. يتحقّق المترجم من تغطيتك لكل متغيِّر — الشمولية هي الفائدة الرئيسية مقابل if/else if.
كيف تبني قيمة choice؟
ابنِ القيمة بتسمية النوع والمتغيِّر وتمرير الحمولة: let r: Result = Result.ok(42) أو let r = Result.err("validation failed"). نوع الحمولة يجب أن يطابق الحمولة المُعلَنة للمتغيِّر — تمرير نوع خاطئ خطأ ترجمة.
ما الفرق بين choice و enum؟
متغيِّرات enum مجرّد تسميات بلا حمولة. متغيِّرات choice كل منها يحمل قيمة من نوع مُعلَن. إن احتجت لربط بيانات بإحدى الحالات (رسالة خطأ، نتيجة ناجحة، رمز مُحلَّل)، استخدم choice. إن كانت الحالات تسميات بحتة، استخدم enum.
لماذا يُفضَّل match على if-else للـ choices؟
match شامل بحكم البناء — يتحقّق المترجم من أن كل متغيِّر مُعالَج، لذا إضافة متغيِّر جديد لاحقًا تُجبرك على تحديث كل موقع يتفرّع على النوع. سلسلة if/else if تمرّ بصمت، فتُخفي الحالة المفقودة حتى تظهر كخطأ في الإنتاج.