جبر المجموعات Set Algebra
اتحاد، تقاطع، حرفيّات، وتوسيع
النبذة
نملك القمّة والقاع. الآن نبني الوسط — حيث تعيش كل الأنواع المفيدة. أداتان فقط: | (اتحاد) و & (تقاطع). لكن خلفهما مفاجأتان تكشفان إن كنت فهمت العدسة حقاً: لماذا الاتحاد يضيّق ما تستطيع فعله رغم أنه يوسّع ما تستطيع تخزينه، ولماذا string & number نوعٌ مستحيل. ثم نُغلق سؤالاً عالقاً من الإقليم ١: لماذا let x = 5 و const x = 5 يعطيان نوعين مختلفين — قصّة التوسيع (widening).
اللغز المستفزّ
لغزان متجاوران، وحدْسك الأوّل سيخطئ في كليهما — وهذا هو المقصود:
اللغز الأول: A | B "أكبر" من A (يحوي قيماً أكثر). إذاً منطقياً يجب أن تستطيع فعل أكثر به، صح؟
typescriptfunction f(x: string | number) { return x.toUpperCase(); // ❌ المترجم يرفض! لماذا؟ }
مجموعة أوسع، قدرات أقلّ. كيف؟
اللغز الثاني: A & B "أصغر" من A. فماذا يكون تقاطع مجموعتين لا تشتركان في أي قيمة؟
typescripttype Weird = string & number; // ما هذا النوع؟ وهل يمكن أن توجد قيمته؟
قبل أن تكمل، خمّن: ما القيمة التي تكون نصاً ورقماً في آنٍ واحد؟
الدرس
الاتحاد A | B = اتحاد المجموعتين
string | number هو حرفياً {كل النصوص} ∪ {كل الأرقام}. قيمةٌ تنتمي للنوع إن كانت في A أو في B. مباشر. الإسناد إلى اتحاد سهل: أي نص يدخل (لأنه في أحد الطرفين)، وأي رقم يدخل.
المفاجأة في الاتجاه الآخر: ماذا تستطيع أن تفعل بقيمةٍ من النوع A | B؟
فكّر كالمُبرهِن. عنده قيمة x يعرف فقط أنها "نص أو رقم"، لا يعرف أيّهما. أي عملية يسمح بها يجب أن تكون آمنة في كلتا الحالتين. toUpperCase() موجودة على النصوص لا الأرقام؛ لو كانت x رقماً لانفجرت. فيرفضها. لكن x.toString() موجودة على الاثنين، فيقبلها.
الثنائية (duality) — احفظ هذه، فهي تربك الجميع: اتحاد مجموعات القيم يقابله تقاطع مجموعات العمليات المتاحة. كلّما وسّعت القيم الممكنة، ضيّقت ما تشترك فيه كلها، فقلّ المسموح. هذا ليس قاعدة عشوائية؛ إنه ينبع مباشرةً من "العملية يجب أن تَصلُح لكل عضو في المجموعة". لتفعل شيئاً خاصاً بالنصوص، عليك أوّلاً أن تُثبت أن x نصٌّ الآن — وهذا التضييق (الإقليم ٤).
هذا يحلّ اللغز الأول تماماً: المجموعة الأوسع تعطي ضماناتٍ أقل مشتركة، فقدراتٍ أقل. الاتساع في التخزين = ضيق في الاستعمال.
التقاطع A & B = تقاطع المجموعتين
A & B هي القيم التي في A و في B معاً. هنا الفائدة الحقيقية مع الكائنات، وستفهمها تماماً في الإقليم ٥، لكن البذرة:
typescripttype Named = { name: string }; type Aged = { age: number }; type Person = Named & Aged; // كائن له name: string و age: number معاً
لماذا التقاطع يُنتج كائناً له كلتا الخاصيتين، رغم أن "تقاطع" يوحي بالأقل؟ لأن عضوية المجموعات معكوسة عن عدد الخصائص: كلّما زادت الشروط (الخصائص المطلوبة)، قلّ عدد الكائنات التي تستوفيها. مجموعة "الكائنات التي لها name" ضخمة. مجموعة "التي لها name و age" أصغر (تقاطع). فالكائن الناتج أغنى بالخصائص لأن مجموعته أضيق. أمسِك هذا الانعكاس جيداً؛ من لا يمسكه يظنّ & و | معكوسين عمّا هما عليه.
المفاجأة: string & number = never
الآن طبّق التقاطع على البدائيّات (primitives). أي قيمة تنتمي لـ string و number معاً؟ لا توجد. لا قيمة في JavaScript هي نصٌّ ورقمٌ في آن. التقاطع خالٍ. ومجموعة خالية اسمها — كما تعلّمت في الإقليم ٢ — never.
typescripttype Weird = string & number; // النوع: never
ليست رسالة خطأ غريبة بل نتيجة حسابية صحيحة: {نصوص} ∩ {أرقام} = ∅ = never. لو رأيت never يظهر فجأة في كودك دون قصد، فغالباً قاطعت من حيث لا تدري نوعين لا يتقاطعان. الآن تقرؤها كرسالة: "طلبتَ قيمةً مستحيلة". العدسة تتنبّأ بالسلوك بدل أن تتفاجأ به — وهذا كل الهدف.
الأنواع الحرفية (literal types) ومنها نبني المجموعات المحدودة
في الإقليم ١ رأينا أن 5 و "hi" و true أنواع (مجموعات مفردة). اتحاد الحرفيّات يعطيك أداة هائلة: مجموعة محدودة من القيم المسموحة بالضبط — وهي إجابة اللغز (ج) العالق من الإقليم ١:
typescripttype Answer = "yes" | "no"; // المجموعة {"yes", "no"} فقط type Dir = "north" | "south" | "east" | "west"; type Bit = 0 | 1;
هذا يحلّ مشكلةً ضخمة كانت تُحلّ في JS بثوابت نصّية هشّة: الآن let a: Answer = "maybe" يُرفض لأن {"maybe"} ⊄ {"yes","no"}. مجموعة معرّفة بالتعداد. (في الإقليم ٤ سترى كيف أن اتحاد الحرفيّات + التضييق يصنعان "discriminated unions" — أقوى نمط نمذجة في اللغة.)
السؤال العالق: التوسيع (widening) — لماذا let و const يختلفان
الآن نسدّ الدين من لغز (د) في الإقليم ١. جرّب وراقب ما يستنتجه المترجم:
typescriptlet a = 5; // النوع المستنتَج: number (وُسِّع!) const b = 5; // النوع المستنتَج: 5 (لم يُوسَّع)
لماذا يختلفان رغم أن القيمة 5 واحدة؟ ارجع للمبدأ الجوهري من الإقليم ١: المترجم يستنتج مجموعة القيم التي قد تصيرها، لا القيمة اللحظية.
const b = 5:bثابت، لا يُعاد إسناده أبداً. مجموعة قيمه المحتملة عبر حياته كلها هي{5}بالضبط. فالنوع الأدقّ5.let a = 5:aقابل لإعادة الإسناد لاحقاً، وغالباً برقم آخر (a = 10). لو ثبّته المترجم على5، لمنعك منa = 10، وهذا غالباً ليس ما تريد منlet. فيتّخذ قراراً عمليّاً: يوسّع المجموعة من{5}إلى أقرب نوع أوسع طبيعي =number. هذا التوسيع (widening).
الخلاصة المبدئية: التوسيع قرارٌ يخمّن "نيّتك" من قابلية التغيير. const يكشف نيّة الثبات فيحفظ النوع ضيّقاً (literal)؛ let يكشف نيّة التغيير فيوسّع. ليس سحراً — إنه المبدأ ذاته: النوع يعكس مدى القيم الممكنة عبر الحياة، لا اللحظة.
وحين تريد ضيقاً صريحاً حتى مع let أو داخل كائن، تستعمل as const:
typescriptlet c = "north" as const; // النوع: "north" (مجموعة مفردة، رغم let) const obj = { dir: "north" }; // dir نوعه string (وُسِّع) const frozen = { dir: "north" } as const; // dir نوعه "north"، وكل شيء readonly
as const تقول للمترجم: "لا توسّع؛ خذ أضيق نوع ممكن وعامله كثابت". ستحتاجها كثيراً حين تبني أنواعاً من قيم. (لاحظ أيضاً أنها تجعل الخصائص readonly — موضوع تربطه بالإقليم ٥.)
اللغز / التمرين
(أ) اشتقّ — بلغة المجموعات والثنائية — كل نتيجة قبل التجربة:
typescriptfunction p(x: string | number) { x.toString(); // ؟ x.toUpperCase(); // ؟ x.toFixed(2); // ؟ (toFixed خاصة بالأرقام) } type T1 = ("a" | "b") & ("b" | "c"); // أي مجموعة؟ عدّد عناصرها. type T2 = ("a" | "b") & ("c" | "d"); // ؟ type T3 = boolean & true; // ؟ (تذكّر boolean = true | false)
(ب) نمذجة. عرّف نوع HttpStatus يقبل فقط الأرقام 200, 301, 404, 500 ولا شيء غيرها. ثم اكتب let s: HttpStatus = 200 (يُقبل) و let bad: HttpStatus = 201 (يجب أن يُرفض). فكّر: أي عملية مجموعات تبني "مجموعة من أربعة أرقام محدّدة"؟
(ج) — لغز التوسيع. تنبّأ بنوع كل تعبير، ثم تحقّق بتمرير الفأرة في المحرّر:
typescriptlet a = "hi"; // ؟ const b = "hi"; // ؟ let c = b; // ؟ (انتبه: نسخنا من const) const arr = [1, 2, 3]; // ؟ (ما نوع العناصر؟) const t = [1, "x"] as const; // ؟ (قارن مع السطر السابق) function id(x = "north") { return x; } // ما نوع x المستنتَج؟ ولماذا ليس "north"؟
لكل سطر، لا تكتفِ بالنوع — اكتب القاعدة التي أنتجته: متى يوسّع المترجم ومتى يحفظ الضيق؟
(د) — الأعمق، تربط جبر المجموعات بالتباين القادم. لديك:
typescripttype A = { tag: "a" }; type B = { tag: "b" }; declare let x: A | B; declare let y: A & B;
أجب بلغة المجموعات: (١) هل x يُسنَد لـ A؟ هل y يُسنَد لـ A؟ لماذا يختلفان؟ (٢) ما نوع y.tag؟ (تلميح ذهني فقط: tag في y يجب أن يكون "a" و "b" معاً — أي تقاطع {"a"} ∩ {"b"}. ماذا ينتج؟ وماذا يقول ذلك عن إمكان وجود قيمة من نوع A & B؟). لا تتعجّل؛ هذا اللغز يكشف أن التقاطع قد يقودك إلى never من حيث لا تتوقّع، وهو درسٌ ستحتاجه في الإقليم ٥ مع الكائنات.
الخلاصة — وصلٌ في الشجرة
أكملنا أدوات بناء المجموعات:
A | B= اتحاد: يوسّع القيم المخزَّنة، يضيّق العمليات المتاحة (ثنائية القيم/العمليات). للاستعمال الخاص بطرفٍ، يلزم تضييق.A & B= تقاطع: يضيّق القيم، يغني الكائنات بالخصائص؛ وبين البدائيّات المتنافرة يساويnever.- الحرفيّات + الاتحاد = مجموعات محدودة معرّفة بالتعداد (
"yes" | "no") — أساس النمذجة الدقيقة. - التوسيع (widening): المترجم يستنتج المجموعة بحسب نيّة التغيّر —
letيوسّع،constيحفظ الضيق، وas constيفرض الأضيق.
ربطنا للوراء: استعملنا قمّة/قاع الإقليم ٢ (never ظهر طبيعياً من التقاطع الفارغ)، وأغلقنا لغزين عالقين من الإقليم ١ (مجموعة "yes"|"no"، وقصّة widening). ربطنا للأمام: قلنا إن string | number لا يُستعمل خاصّاً قبل أن تُثبت أيّ طرفٍ هو الآن — وهذا حرفياً الإقليم ٤.
بذرة غموض: لدينا الآن string | number، لكن لا نستطيع لمس أيٍّ منهما خاصّاً. هذا مزعج عملياً — فما فائدة نوعٍ لا نقدر استعماله؟ الجواب أن المترجم يستطيع أن يتتبّع منطق برنامجك ويُقلّص المجموعة سطراً بسطر: داخل if (typeof x === "string") تصير x نصاً نقيّاً، تلقائياً. كيف يقرأ المترجم الـ if؟ وكيف يربط فحصاً في JavaScript بعملية طرحٍ على مجموعة؟ هذا أذكى ما في اللغة. للإقليم ٤.
التالي: 04-narrowing.md