اذهب إلى المحتوى

معالجة سلسلة من العناصر باستخدام المكررات iterators في لغة رست


Naser Dakhel

يسمح لك نمط المكرّر iterator pattern بإنجاز مهمة ما على سلسلة من العناصر بصورةٍ متتالية، والمكرّر مسؤول عن المنطق الخاص بالمرور على كل عنصر وتحديد مكان انتهاء السلسلة، إذ ليس من الواجب عليك إعادة تطبيق هذا المنطق بنفسك عند استخدام المكررات.

المكررات في رست كسولة، بمعنى أنها لا تمتلك أي تأثير حتى تستدعي أنت التابع الذي يستخدم المكرر، فعلى سبيل المثال نُنشئ الشيفرة 10 مكرّرًا على العناصر الموجودة في الشعاع ‏ v1 باستدعاء التابع iter المعرّف على Vec<T>‎، ولا تفعل تلك الشيفرة البرمجية أي شيء مفيد.

    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

[الشيفرة 10: إنشاء مكرر]

نخزّن المكرر في المتغير v1_iter، ويمكننا استخدامه بطرق عدّة بعد إنشائه. على سبيل المثال، استخدمنا حلقة for في الشيفرة 5 من فصل سابق للمرور على مصفوفة وذلك لتنفيذ شيفرة برمجية على كل من عناصرها، وفي الحقيقة كان المكرّر موجودًا ضمنيًا في تلك الشيفرة لتحقيق ذلك إلا أننا لم نتكلم عن ذلك سابقًا.

نفصل في الشيفرة 11 إنشاء المكرر من استخدامه في الحلقة for، فعندما تُستدعى الحلقة for باستخدام المكرر v1_iter، يُستخدم كل عنصر في المكرر مرةً تلو الأخرى في كل دورة للحلقة، وهذا يطبع كل قيمة من قيم العناصر.

    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {}", val);
    }

[الشيفرة 11: استخدام مكرر في حلقة for]

يمكنك كتابة شيفرة برمجية تفعل الأمر نفسه في لغات البرمجة التي لا تستخدم المكررات في مكتبتها القياسية وذلك عن طريق البدء بالمتغير من الدليل‏ 0 واستخدام قيمة المتغير يمثابة دليلٍ للشعاع للحصول على قيمة ومن ثم زيادة قيمة المتغير في الحلقة حتى تصل قيمته لعدد العناصر الكلية في الشعاع.

تتعامل المكررات مع المنطق البرمجي نيابةً عنك مما يقلل من الشيفرات البرمجية المتكررة التي من الممكن أن تتسبب بأخطاء، وتعطيك المكررات مرونةً أكبر في التعامل مع المنطق ذاته في الكثير من أنواع السلاسل وليس فقط هياكل البيانات data structures التي يمكنك الوصول إلى قيمها باستخدام دليل مثل الشعاع. دعنا نلقي نظرةً إلى كيفية تحقيق المكررات لكل هذا.

سمة Iterator وتابع next

تطبّق جميع المكررات السمة Iterator المُعرّفة في المكتبة القياسية، ويبدو تعريف السمة شيء مماثل لهذا:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}

لاحظ أن هذا التعريف يستخدم صيغتان جديدتان، هما: type Item و Self::Item اللتان تُعرِّفان نوعًا مترابطًا associated type مع هذه السمة. سنتحدث عن الأنواع المترابطة بالتفصيل لاحقًا، ويكفي للآن معرفتك أن هذه الشيفرة البرمجية تقول أن تطبيق السمة Iterator يتطلب منك تعريف نوع Item أيضًا وهذا النوع مُستخدم مثل نوع مُعاد من التابع next، وبكلمات أخرى، سيكون النوع Item هو النوع المُعاد من المكرّر.

تتطلب السمة Iterator ممن يطبّقها فقط أن يعرّف تابعًا واحدًا هو next، الذي يُعيد عنصرًا واحدًا من المكرر كل مرة ضمن Some وعندما تنتهي العناصر (ينتهي التكرار)، يُعيد None.

يمكننا استدعاء التابع next على المكررات مباشرةً، وتوضح الشيفرة 12 القيم المُعادة من الاستدعاءات المتعاقبة على المكرّر باستخدام next وهو المكرر الموجود في الشعاع.

اسم الملف: src/lib.rs

    #[test]
    fn iterator_demonstration() {
        let v1 = vec![1, 2, 3];

        let mut v1_iter = v1.iter();

        assert_eq!(v1_iter.next(), Some(&1));
        assert_eq!(v1_iter.next(), Some(&2));
        assert_eq!(v1_iter.next(), Some(&3));
        assert_eq!(v1_iter.next(), None);
    }

[الشيفرة 12: استدعاء التابع next على المكرر]

لاحظ أننا احتجنا لإنشاء v1_iter متغيّر mutable، إذ يغيّر استدعاء التابع next على مكرر الحالة الداخلية التي يستخدمها المكرر لتتبع مكانه ضمن السلسلة، وبكلمات أخرى، تستهلك consumes الشيفرة البرمجية المكرّر، إذ يستهلك كل استدعاء للتابع next عنصرًا واحدًا من المكرر. لم يكن هناك أي حاجة لإنشاء v1_iter متغيّر عندما استخدمنا حلقة for لأن الحلقة أخذت ملكية ownership المكرر v1_iter وجعلته متغيّرًا ضمنيًا.

لاحظ أيضًا أن القيم التي نحصل عليها من استدعاءات التابع next هي مراجع ثابتة immutable references للقيم الموجودة في الشعاع، ويعطينا التابع iter مكرًرًا على المراجع الثابتة. إذا أردنا إنشاء مكرر يأخذ ملكية v1 ويُعيد القيم المملوكة فيمكننا استدعاء into_iter بدلًا من iter، ويمكننا بصورةٍ مماثلة استدعاء iter_mut بدلًا من iter إذا أردنا المرور على مراجع متغيّرة.

توابع تستهلك المكرر

للسمة Iterator العديد من التوابع في تطبيقها الافتراضي والموجودة في المكتبة القياسية، ويمكنك الاطّلاع على هذه التوابع عن طريق النظر إلى توثيق واجهة المكتبة القياسية البرمجية للسمة Iterator. تستدعي بعض هذه التوابع التابع next في تعريفها وهو السبب في ضرورة تطبيق التابع next عند تطبيق السمة Iterator.

تُسمّى التوابع التي تستدعي التابع next بالمحوّلات المُستهلكة consuming adaptors لأن استدعائها يستهلك المكرر. يُعد تابع sum مثال على هذه التوابع، فهو يأخذ ملكية المكرر ويمرّ على عناصره بصورةٍ متتالية مع استدعاء next مما يتسبب باستهلاك المكرر، وبينما يمرّ على العناصر فهو يجمع كل عنصر إلى قيمة كلية، ثم يعيد القيمة الكلية في النهاية. تحتوي الشيفرة 13 على اختبار يوضح استخدام التابع sum:

اسم الملف: src/lib.rs

    #[test]
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];

        let v1_iter = v1.iter();

        let total: i32 = v1_iter.sum();

        assert_eq!(total, 6);
    }

[الشيفرة 13: استدعاء التابع sum للحصول على القيمة الكلية لجميع العناصر في المكرر]

لا يُسمح لنا باستخدام v1_iter بعد استدعاء sum لأن sum يأخذ ملكية المكرّر الذي نستدعي sum عليه.

التوابع التي تنشئ مكررات أخرى

محولات المكرر iterator adaptors هي توابع مُعرّفة على السمة Iterator ولا تستهلك المكرر، بل تُنشئ مكررات مختلفة بدلًا من ذلك عن طريق تغيير بعض خصائص المكرر الأصل.

توضح الشيفرة 14 مثالًا عن استدعاء تابع محول مكرر map وهو تابع يأخذ مغلّفًا closure ويستدعيه على كل من العناصر بصورةٍ متتالية. يُعيد التابع map مكررًا جديدًا يُنشئ عناصرًا مُعدّلٌ عليها، ويُنشئ المغلف في هذه الحالة مكررًا جديدًا يزيد قيمة عناصره بمقدار 1 لكل عنصر في الشعاع:

اسم الملف: src/main.rs

    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);

[الشيفرة 14: استدعاء محول المكرر map لإنشاء مكرر جديد]

إلا أن الشيفرة البرمجية السابقة تعطينا إنذارًا:

$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

warning: `iterators` (bin "iterators") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

لا تفعل الشيفرة 14 أي شيء، إذ أن المغلف الذي حددناه لا يُستدعى أبدًا، ويذكرنا الإنذار بسبب ذلك: إذ أن محولات المكرر كسولة ونحتاج لاستهلاك المكرر هنا.

نستخدم التابع collect لتصحيح هذا الإنذار واستهلاك المكرر وهو ما استخدمناه في (الشيفرة 1 من فصل سابق) باستخدام env::args، إذ يستهلك هذا التابع المكرر ويجمّع القيم الناتجة إلى نوع بيانات تجميعة collection data type.

نجمع في الشيفرة 15 النتائج من عملية المرور على المكرر والمُعادة من استدعاء التابع map على الشعاع، وسيحتوي هذا الشعاع في نهاية المطاف على جميع عناصره الموجودة مع زيادة على قيمة كل منها بمقدار 1.

اسم الملف: src/main.rs

    let v1: Vec<i32> = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

    assert_eq!(v2, vec![2, 3, 4]);

[الشيفرة 15: استدعاء التابع map لإنشاء مكرر جديد ومن ثم استدعاء التابع collect لاستهلاك المكرر الجديد وإنشاء شعاع]

يمكننا تحديد أي عملية نريد إنجازها على كل عنصر بالنظر إلى أن map تتلقى مغلفًا بمثابة وسيط لها. هذا مثال عظيم عن كيفية إنجاز مهام معقدة بطريقة مقروءة، ولأن جميع المكررات كسولة، فهذا يعني أنك بحاجة استدعاء واحد من توابع المحولات المستهلكة للحصول على نتائج استدعاءات محولات المكرر.

استخدام المغلفات التي تحصل على القيم من بيئتها

تأخذ الكثير من محولات المكرر المغلفات مثل وسطاء لها، وستحصل هذه المغلفات التي تُحدد مثل وسطاء لمحولات المكرر على قيم من بيئتها غالبًا.

نستخدم في هذا المثال التابع filter الذي يأخذ مغلّفًا، ويحصل المغلف على عنصر من المكرر ويُعيد bool، وتُضمّن هذه القيمة في التكرار المُنشئ بواسطة filter إذا أعاد المغلف القيمة true، وإذا أعاد المكرر القيمة false فلن تُضمَّن القيمة.

نستخدم في الشيفرة 16 التابع filter بمغلّف يحصل على المتغير shoe_size من بيئته للمرور على تجميعة من نسخ instances الهيكل‏ Shoe، وسيُعيد فقط الأحذية ذات مقاس محدد.

اسم الملف: src/lib.rs

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 13,
                style: String::from("sandal"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ];

        let in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}

[الشيفرة 16: استخدام التابع filter مع مغلف يحصل على القيمة shoe_size]

تأخذ الدالة shoes_in_size ملكية شعاع الأحذية وقياس الحذاء مثل معاملات، وتُعيد شعاعًا يحتوي على الأحذية بالمقاس المحدد.

نستدعي into_iter في متن الدالة shoes_in_size لإنشاء مكرر يأخذ ملكية الشعاع، ثم نستدعي filter لتحويل المكرّر إلى مكرر جديد يحتوي فقط على عناصر يُعيد منها المغلّف القيمة true.

يحصل المغلف على المعامل shoe_size من البيئة ويقارنه مع قيمة كل مقاس حذاء مع إبقاء الأحذية التي يتطابق مقاسها، وأخيرًا يجمع استدعاء collect القيم المُعادة بواسطة محول المكرر إلى شعاع يُعاد من الدالة.

يوضح هذا الاختبار أنه عندما نستدعي shoes_in_size، فنحن نحصل فقط على الأحذية التي تتطابق مقاساتها مع القيمة التي حددناها.

ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...