HeadlinesBriefing favicon HeadlinesBriefing.com

تحليل لا تحقق في روست: المتجهات غير الفارغة

Hacker News •
×

مثل العديد من المبرمجين، أجد مقال أليكسيس كينغ عن 'تحليل لا تحقق' مثيرًا للاهتمام، لأنه يعطي اسمًا لطريقة تبدو مألوفة ومهمة - طريقة لاحظتها واستخدمتها في الماضي دون تسميتها صراحةً. هذا المقال هو مراجعة لنمط 'تحليل لا تحقق' المطبق على لغة البرمجة روست (المقال الأصلي يستخدم هاسكل). كنت مهتمًا بشكل خاص بالعثور على أمثلة تعليمية لهذا النمط في مكتبة روست القياسية وفي مشاريع أخرى معروفة. دون تكرار المقال الأصلي (يرجى قراءته أولاً!)، إليك جوهره. لنأخذ مثال المتجه الكلاسيكي؛ طريقة first الخاصة به تعيد Option<&T>. لماذا؟ لأن المتجه ليس مضمونًا أن يحتوي على أي عناصر، فماذا يحدث إذا تم استدعاء first على متجه فارغ؟ إعادة Option في هذه الحالة هي ممارسة شائعة في روست [1]، مع وجود بناء نحوي مريح لقبول نتائج الدوال التي تعيد Option واتخاذ قرار بشأن الخطوة التالية. إذن ما هي المشكلة؟ تخيل أن لدينا دالة لقراءة بعض مسارات التكوين من متغير بيئة، مع فرض قيد بأن القائمة لا يمكن أن تكون فارغة: use anyhow::{Result, ensure}; fn get_configuration_directories() -> Result<Vec<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories: Vec<Path Buf> = value.split(',').map(str::trim).map(Path Buf::from).collect(); ensure!(!directories.is_empty(), "empty CONFIG_DIRS"); Ok(directories) } حتى الآن، كل شيء على ما يرام. دعنا ننظر الآن إلى الاستخدام النموذجي لهذه الدالة: fn main() -> Result<()> { let config_dirs = get_configuration_directories()?; match config_dirs.first() { Some(cache_dir) => initialize_cache(cache_dir), None => unreachable!("already checked that CONFIG_DIRS is non-empty"); } Ok(()) } بمجرد أن تعيد get_configuration_directories نتيجة ناجحة، نكون مضمونين بأن المتجه ليس فارغًا. ومع ذلك، إذا أردنا الحصول على العنصر الأول من هذا المتجه، يجب علينا استخدام طريقة first التي تعيد Option<&T>. وبالتالي، نُجبر مرة أخرى على التعامل مع حالة محتملة فارغة (حيث تكون الخيار None). كما ينص المقال الأصلي، فإن هذا له عدد من المشاكل المتعلقة بوضوح الكود، والآثار المحتملة على الأداء، وقنبلة موقوتة إذا تم تغيير القيد في get_configuration_directories. المشكلة الأساسية هي أن المتجه هو في الأساس نوع يمكن أن يكون فارغًا؛ يمكننا كتابة تعليق "هذا لا يمكن أن يكون فارغًا، أعدك!" على جميع الأكواد ذات الصلة، ولكن لا يوجد ما يتحقق منه رسميًا. نوع للمتجه "غير الفارغ" الحل هو الاستفادة من نظام الأنواع لفرض قيد تم إنشاؤه حديثًا. يمكننا استخدام نوع منفصل للمتجه "الذي لا يمكن أن يكون فارغًا"؛ في الواقع، مثل هذه الأنواع موجودة بالفعل في العديد من مكتبات روست - على سبيل المثال، nonempty: pub struct Non Empty<T> { pub head: T, pub tail: Vec<T>, } هذا النوع ليس لديه منشئ يسمح بـ "عدم وجود عناصر"؛ طريقة new الخاصة به تأخذ عنصرًا واحدًا، وطريقة first الخاصة به تعيد &T دون خيار: pub const fn new(e: T) -> Self { Self::singleton(e) } pub const fn singleton(head: T) -> Self { Non Empty { head, tail: Vec::new() } } pub const fn first(&self) -> &T { &self.head } بقية المكتبة تعمل على جعل Non Empty يتصرف بأقرب ما يمكن إلى متجه عادي، من خلال تنفيذ العديد من الخصائص المفيدة، بالإضافة إلى تحويلات مثل: pub fn from_vec(mut vec: Vec<T>) -> Option<Non Empty<T>> { if vec.is_empty() { None } else { let head = vec.remove(0); Some(Non Empty { head, tail: vec }) } } دعنا نرى كيف سيبدو كود get_configuration_directories الخاص بنا إذا كان يعيد Non Empty بدلاً من متجه عادي: fn get_configuration_directories() -> Result<Non Empty<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories = value.split(',').map(str::trim).map(Path Buf::from).collect(); let Some(directories) = Non Empty::from_vec(directories) else { bail!("CONFIG_DIRS cannot be empty"); }; Ok(directories) } لاحظ استخدام Non Empty::from_vec هنا - هذا هو المكان الذي يتم فيه فرض القيد. الآن...

سؤال