دستور الملكية
Rust

في fullstack تعلّمتَ: الإغلاق (closure) = كود + بيئة مُلتقَطة على الـ heap، وهي الآلية التي تفسّر callbacks وhooks في JS. الجزء الذي لم تحتَجه هناك: JS تلتقط البيئة بطريقة واحدة دائمًا — مرجع مشترك لصندوق قابل للتغيير على الـ heap، يديره الـ GC. Rust ترفض إجابة واحدة موحّدة — لأنها ترفض أي افتراض لم تثبته البديهيتان. هذا الإقليم يريك كيف تتحلل "الالتقاط" إلى نفس الثلاثية التي تعيش معك منذ الإقليم ٠١، وكيف أن for نفسها — التي تكتبها منذ سطرك الأول — كانت طوال الوقت واجهة لآلة لم ترها بعد.


الدرس ١: الإغلاقات — ثلاثة أزياء لثلاثية واحدة

اللغز

أنت تعرف من الإقليمين ٠١-٠٢ أن أي وصول لقيمة له ثلاثة أشكال فقط لا رابع لها: استعارة قراءة (&T)، استعارة حصرية (&mut T)، أو تمليك كامل (T). والإغلاق — بحكم تعريفه — "يلتقط" متغيرات من البيئة المحيطة به ليستعملها لاحقًا. كم طريقة ممكنة إذًا لالتقاط أي متغيّر معطى؟ أجب قبل المتابعة — الجواب سيفسّر لك عدد أنواع الإغلاقات في اللغة كاملًا، بلا حفظ.

الحل: Fn / FnMut / FnOnce

let s = String::from("hi");
let read_it = || println!("{s}");        // يلتقط s بـ &  → Fn
read_it(); read_it();                     // يُستدعى مرات بلا مشكلة

let mut count = 0;
let mut bump = || { count += 1; };        // يلتقط count بـ &mut → FnMut
bump(); bump();                           // مُستدعى مرات، لكن يحتاج وصولًا حصريًا لنفسه

let name = String::from("bye");
let consume = move || { let _owned = name; };  // move يجبر الالتقاط بالتمليك → FnOnce
consume();
// consume();   // error: هذا closure من نوع FnOnce — والقيمة الملتقَطة نُقلت بالفعل

المترجم يستنتج أضيق التقاط ممكن تلقائيًا من جسم الإغلاق (تمامًا كما يستنتج نوع let x = 5) — لا تكتب Fn/FnMut/FnOnce بيدك أبدًا عند التعريف، فقط عند اشتراطها كقيد على معامل دالة. الكلمة move تجبر الالتقاط بالتمليك حتى لو كانت الاستعارة كافية — ستحتاجها كل مرة تريد فيها لإغلاق أن يعيش أطول من النطاق الحالي (تلميح: هذا بالضبط ما يحتاج إليه خيط جديد في الإقليم ٠٨).

الثلاثة تُشكّل تراتبية، لا مجموعات منفصلة — تذكّر من TS: subtype = subset. هنا: كل Fn هي أيضًا FnMut (من يستطيع الاكتفاء بقراءة، يستطيع منطقيًا العمل ضمن عقد أضعف يطلب فقط استدعاءً متكررًا)، وكل FnMut هي أيضًا FnOnce (من يستطيع أن يُستدعى مرات، يستطيع أن يُستدعى مرة واحدة على الأقل). الأقوى ضمانًا (Fn) هو الأضيق نطاق تطبيق فعليًا؛ الأضعف ضمانًا (FnOnce) هو الأعم.

الإغلاق كنوع، لا كصندوق موحّد

في JS كل دالة/closure قيمة من "نوع" واحد غامض. في Rust كل إغلاق نوعه الخاص المجهول، حجمه على الـ stack يساوي بالضبط حجم ما التقطه (صفر إن لم يلتقط شيئًا — عندها هو حرفيًا مؤشر دالة عادي). هذا امتداد مباشر لزيرو-كوست الإقليم ٠٤: لا صندوق heap إجباري، لا GC يتتبعه — إلا إن طلبتَ ذلك صراحة:

fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y }   // static — نوع محدد وقت الترجمة
fn pick(op: char) -> Box<dyn Fn(i32, i32) -> i32> {                // dynamic — قائمة/تخزين غير متجانس
    match op {
        '+' => Box::new(|a, b| a + b),
        _   => Box::new(|a, b| a - b),
    }
}

نفس ثنائية impl Trait مقابل dyn Trait من الإقليم ٠٤ بالحرف — لأن الإغلاق، تحت غطاء الصياغة الخاصة |x| ...، هو نوع يطبّق trait (أحد الثلاثة)، لا شيء أكثر سحرًا من ذلك.


الدرس ٢: المنتِج (Iterator) — الآلة التي كانت تعمل تحت for طوال الوقت

اللغز

عد لسؤال الإقليم ٠٠ المعلَّق: لماذا حذفت Rust حلقة C الثلاثية for(;;) تمامًا؟ فكّر في كل bug فهرسة عشتَه في Holberton: <= بدل <، نسيان i++، قراءة arr[n] بدل arr[n-1]. كلها أخطاء حساب يدوي لموضع التوقف. اسأل: ماذا لو لم يكن "متى تتوقف الحلقة" حسابًا تكتبه أنت في كل مرة، بل سؤالًا تطرحه على البنية نفسها، تجيب عنه بنوع تعرفه أنت من الإقليم ٠٣ تمامًا؟

الحل: Iterator

trait Iterator {
    type Item;                                  // نوع مرتبط — رأيته يلمح في ٠٤
    fn next(&mut self) -> Option<Self::Item>;   // Option! نفس التي بنيت حدسها في ٠٣
}

هذا التعريف الكامل تقريبًا (المكتبة القياسية تضيف عشرات الطرق الافتراضية فوقه، مبنية جميعًا على next وحدها). for ليست جملة تحكّم خاصة — إنها سكّر نحوي كامل فوق هذه الآلية:

for x in 0..5 { println!("{x}"); }

// يوازي تمامًا:
let mut it = (0..5).into_iter();
while let Some(x) = it.next() {
    println!("{x}");
}

"متى تتوقف" لم يعد حسابًا يدويًا — أصبح None. المستحيل نحويًا صار: قراءة خارج الحدود، لأن لا فهرسة يدوية أصلًا في هذا النمط. هذا الجواب الكامل على لغز الإقليم ٠٠.

ابنِ واحدًا بيدك أولًا

قبل الاتكال على مكتبة الأدوات الجاهزة، ابنِ منتِجًا من الصفر — هكذا فقط تفهم أن map/filter ليست سحرًا بل دوال عادية فوق next:

struct Countdown(u32);
impl Iterator for Countdown {
    type Item = u32;
    fn next(&mut self) -> Option<u32> {
        if self.0 == 0 { None } else { self.0 -= 1; Some(self.0 + 1) }
    }
}
for n in (Countdown(3)) { println!("{n}"); }   // 3 2 1

الكسل (laziness) وصفر التخصيص

let total: i32 = (1..=1_000_000).filter(|n| n % 3 == 0).map(|n| n * 2).sum();

لا شيء "يحدث" عند كتابة .filter(...).map(...) — هذه تركّب سلسلة منتِجين متداخلين، كل واحد يغلّف الذي قبله، بلا تخصيص أي Vec وسيط. الحساب الفعلي يبدأ فقط عند نداء مُستهلِك (consumer) مثل .sum() أو .collect() أو for، ويسحب عنصرًا عنصرًا عبر السلسلة كاملة قبل طلب التالي. المترجم — بفضل monomorphization (الإقليم ٠٤) — يحوّل هذه السلسلة إلى حلقة واحدة مضغوطة بلا أي طبقة تنسيق إضافية، مطابقة تمامًا لما كنت لتكتبه يدويًا بحلقة C واحدة يدوية. زيرو-كوست، مرة أخرى، مطبَّقة هنا بالحرف.

جولة سريعة على الأشهر (لا شرح لكل واحدة — واضحة من اسمها وتوقيعها؛ جرّبها): map, filter, enumerate (يُرفق فهرسًا usize — بديل آمن لحساب i اليدوي)، zip, take(n), skip(n), rev, fold(init, f) (تعميم sum/ sum نفسها)، collect::<Vec<_>>() (لاحظ ::<> — turbofish، تحسم للمترجم أي نوع تريد التجميع فيه حين لا يكفي السياق للاستنتاج).

الثلاثية تظهر مجددًا — هذه المرة في for

for x in &v      { /* x: &T     — استعارة قراءة، يستدعي .iter()     */ }
for x in &mut v  { /* x: &mut T — استعارة حصرية، يستدعي .iter_mut() */ }
for x in v       { /* x: T      — تمليك كامل، يستدعي .into_iter()، v تموت بعدها */ }

توقف هنا. هذه نفس الثلاثية بالحرف التي رأيتها في استقبال الطرق (&self/&mut self/self، الإقليم ٠٤) وفي التقاط الإغلاقات (Fn/ FnMut/FnOnce، قبل قليل). ليست ثلاث ميزات منفصلة تحفظها — إنها مظهر واحد متكرر لبديهيتَي الإقليمين ٠١-٠٢، يعاد توظيفه في كل موضع تحتاج فيه اللغة أن تسأل "كيف تصل لهذه القيمة؟". حين تلتقي بنمط ثلاثي جديد لاحقًا في اللغة، اسأل أولًا: أهذه الثلاثية نفسها متنكرة مرة أخرى؟ غالبًا الجواب نعم.


الدرس ٣: حل مطبّ العربي — UTF-8 من جذوره

في ٠٢-ج طُلب منك أن تراقب مطبًّا مع النصوص العربية بلا إصلاح. حان وقت التفسير الكامل.

تحقّق الآن: عد لحل ٠٢-ج، وإن كنتَ استعملتَ char_indices() بحذافيرها (لا حسابًا يدويًا فوقها) فستجده يعمل مع العربية بلا تعديل — لأن الحدود كانت محروسة طوال الوقت. إن لم يعمل، الآن تعرف بالضبط أين الافتراض الخاطئ كان يختبئ. (ملاحظة أخيرة تستحق أن تُعلَّق لا أن تُشرح الآن: حتى char ليست نهاية القصة — بعض ما يبدو "حرفًا واحدًا" للعين البشرية (رموز مركّبة، تشكيل) قد يتكوّن من عدة char معًا؛ يتجاوز عمق هذا المنهج.)


معلم البناء: أداة بحث نصية من الصفر

معلم بناء
معلم البناء: أداة بحث نصية من الصفر

ابنِ أداة سطر أوامر — سمّها ما شئت — تُشبه grep مصغّرة. هذا معلم لا لغز مُغلَق: صمّم تفاصيلك، لكن التزم بهذه المتطلبات لأنها تُجبرك على تركيب كل ما تعلمته من الإقليم ٠٠ حتى الآن في نظام واحد متماسك:

الوظيفة:

القيود المعمارية (إلزامية):

لا حلّ مرجعيًا لهذا المعلم في المنهج. اختبر أداتك يدويًا على ملفات حقيقية حتى تثق بها.


الألغاز

لغز ٠٦-أ: محكمة الإغلاقات. لكل إغلاق أدناه: صنّفه (Fn/FnMut/FnOnce) قبل التشغيل، مع سبب مرتبط تحديدًا بكيفية التقاطه للمتغيّر، ثم تحقّق باستدعائه مرتين متتاليتين (أو بتمريره لدالة تشترط F: Fn()):

let v = vec![1, 2, 3];
let a = || println!("{v:?}");

let mut total = 0;
let b = |n: i32| { total += n; };

let s = String::from("owned");
let c = move || s.len();

let mut buf = String::new();
let d = move |x: &str| { buf.push_str(x); buf.clone() };

لغز ٠٦-ب: منتِج فيبوناتشي. ابنِ struct Fib { a: u64, b: u64 } يطبّق Iterator<Item = u64> وينتج متتالية فيبوناتشي بلا حدّ أعلى مدمج (منتِج لا نهائي — مسموح ومُستعمَل اصطلاحيًا). ثم استعمله عبر تركيب منتِجين جاهزين فقط (take, filter, map, sum, ...) — بلا أي حلقة for/while يدوية — لطباعة: أول ١٠ حدود، ثم مجموع الحدود الزوجية من أول ٢٠ حدًّا.

لغز ٠٦-ج: مصنع الفلاتر. اكتب fn make_filter(min: i32) -> impl Fn(&i32) -> bool تُعيد إغلاقًا يفحص "هل العدد ≥ min". ثم اكتب fn make_filters(mins: &[i32]) -> Vec<Box<dyn Fn(&i32) -> bool>> تبني قائمة من هذه الفلاتر بعتبات مختلفة (استعمل dyn — فكّر لماذا impl Fn وحدها لا تكفي هنا؛ اربط جوابك بجدول Static/Dynamic من ٠٤). استعمل القائمة الناتجة لتصفية Vec<i32> واحد عبر كل الفلاتر تباعًا (منتِج واحد يستهلك القائمة، بلا فهرسة يدوية).


الخلاصة

بذرة الغموض

قبل أن تصل هناك (الإقليم ٠٨)، عقدة أخرى تنتظر: كل بنية بيانات بنيتَها حتى الآن (Vec, struct, enum) لها مالك واحد بالضبط بصرامة. ماذا لو احتجتَ فعليًا مالكَين اثنين مشروعَين لنفس القيمة — مثل عقدة في شجرة يشير إليها أبواها كلاهما؟ البديهية الأولى تبدو صارمة جدًا لتسمح بهذا. الإقليم القادم يريك أنها لم تكسر قط — لكنها تعرف كيف تتفاوض.