اختبارات أو أنواع؟ - نسخة الصدأ

منذ يومين ، نشر 0xd34df00d ترجمة لمقال هنا يصف ما يمكنك تعلمه عن وظيفة بلغات مختلفة ، إذا كنت تعتبرها " مربعًا أسودًا" دون استخدام معلومات حول تنفيذها (ولكن ، بالطبع ، دون إيقافها عن استخدام برنامج التحويل البرمجي). بالطبع ، المعلومات الواردة تعتمد اعتمادًا كبيرًا على اللغة - تم أخذ أربعة أمثلة في المقال الأصلي:


  • Python - المعلومات المكتوبة بشكل ديناميكي ، الحد الأدنى ، الاختبارات فقط تعطي بعض التلميحات ؛
  • ج - ضعيف ثابت ثابت ، وليس الكثير من المعلومات ؛
  • Haskell - مطبوع بشكل ثابت ثابت ، مع وظائف نقية ، والمزيد من المعلومات ؛
  • Idris هي لغة ذات أنواع تابعة ، وهناك معلومات كافية لإثبات صحة الوظيفة أثناء الترجمة.

"هناك C ، هناك Haskell ، ولكن أين هو Rust؟!" - على الفور تم طرح السؤال. الجواب تحت الخفض.


أذكر حالة المشكلة:


واسمحوا قائمة وبعض المعطى. يجب عليك إرجاع فهرس هذه القيمة في القائمة أو الإشارة إلى أن هذه القيمة ليست في القائمة.

من أجل الصبر - كل الخيارات التي تمت مناقشتها أدناه يمكن رؤيتها في ملعب Rust .
دعنا نذهب!


بحث بسيط


سنبدأ بتوقيع ساذج ، والذي يختلف في الحقيقة عن الكود C فقط في بعض العناصر الاصطلاحية:


fn foo(x: &[i32], y: i32) -> Option<usize> { // 10000    } 

ماذا نعرف عن هذه الميزة؟ حسنا ... ليس كثيرا ، في الواقع. بالطبع ، يعد Option<usize> في قيم الإرجاع أفضل بكثير مما قدمه لنا C ، ولكن لا يزال لدينا أي معلومات حول دلالات الوظيفة. على وجه الخصوص ، ليس هناك ما يضمن أنه لن يكون هناك أي آثار جانبية ، ولا يمكن أن يكون هناك أي طريقة للتحقق من السلوك المتوقع.


يمكن اختبار مكتوبة بشكل صحيح إصلاح الوضع؟ نحن ننظر:


 #[test] fn test() { assert_eq!(foo(&[1, 2, 3], 2), Some(1)); assert_eq!(foo(&[1, 2, 3], 4), None); } 

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


استخدم الوراثة ، لوقا!


ولكن هل من الجيد حقًا أننا مضطرون لاستخدام أرقام 32 بت موقعة فقط؟ الفوضى. نصلح:


 fn foo<El>(x: &[El], y: El) -> Option<usize> where El: PartialEq, { // 10000    } 

آها! هذا بالفعل شيء. الآن يمكننا أخذ شرائح ، تتكون من أي عناصر يمكننا مقارنتها من أجل المساواة. تعد تعدد الأشكال الصريح دائمًا أفضل من الضمني ، ودائمًا ما يكون أفضل من لا شيء ، أليس كذلك؟


ومع ذلك ، قد تنجح مثل هذه الوظيفة في اجتياز الاختبار التالي بشكل غير متوقع:


 fn refl<El: PartialEq + Copy>(el: El) -> Option<usize> { foo(&[el], el) // should always return Some(0), right? } #[test] fn dont_find_nan() { assert_eq!(refl(std::f64::NAN), None); } 

يشير هذا على الفور إلى وجود بعض العيوب من جانبنا ، لأنه وفقًا للمواصفات الأصلية ، سيتعين على مثل هذه المكالمة إرجاع Some(0) . بالطبع ، تكمن المشكلة هنا في خصوصية الأنواع ذات المقارنة المحددة جزئيًا بشكل عام والعوامات بشكل خاص.
لنفترض الآن أننا نريد التخلص من مثل هذه المشكلة - لهذا نقوم بتشديد متطلبات النوع El:


 fn foo<El>(x: &[El], y: El) -> Option<usize> where El: Eq, { // 10000    } 

نطالب الآن ليس فقط إمكانية المقارنة من أجل المساواة - نطالب بأن تكون هذه المقارنة علاقة تكافؤ . يؤدي هذا إلى تضييق نطاق المعلمات المحتملة إلى حد ما ، ولكن الآن يشير كلا النوعين والاختبارات (وإن لم تكن تشير صراحة) إلى أن السلوك المتوقع يجب أن يقع بالفعل في المواصفات.


الانحدار: نريد أن نذهب أكثر عام!

لا يرتبط هذا الخيار بالمهمة الأصلية ، ولكن ، في رأيي ، هو توضيح جيد للمبدأ: "كن ليبراليًا في ما تقبله ، وكن محافظًا فيما تفعله". بمعنى آخر: إذا كانت هناك فرصة ، دون المساس ببيئة العمل والأداء ، لجعل نوع القيم المقبولة أكثر عمومية - فمن المنطقي أن تفعل ذلك بالضبط.


النظر في هذا الخيار:


 fn foo<'a, El: 'a>(x: impl IntoIterator<Item = &'a El>, y: El) -> Option<usize> where El: Eq, { // 10000    } 

ماذا نعرف عن هذه الوظيفة الآن؟ كل شيء هو نفسه ، فقط الآن لا يقبل قائمة أو شريحة كإدخال ، ولكن بعض الكائنات التعسفية ، والتي يمكن إجراؤها لإعطاء روابط بالتناوب مع كائنات من النوع El ومقارنتها بالجهاز المطلوب: كان التناظرية في Java ، إذا كنت أتذكر بشكل صحيح ، ستكون وظيفة Iterable<Comparable> مقياس Iterable<Comparable> .


كما كان من قبل ، فقط أكثر صرامة


ومع ذلك ، على سبيل المثال ، الضمانات التي يقدمها المترجم في المراحل المعروفة بالفعل ليست كافية بالنسبة لنا. أو ، دعنا نقول ، إننا لا نريد (لسبب أو لآخر) الدخول في كومة ، لكننا نريد العمل في المجموعة - مما يعني أننا بحاجة إلى مجموعة بدلاً من المتجه - لكن في الوقت نفسه نريد تعميم التعليمات البرمجية الخاصة بنا على أحجام مختلفة من المصفوفة . أو نريد تحسين الوظيفة إلى أقصى حد ممكن لكل حجم محدد من قائمة المدخلات.

باختصار ، نحتاج إلى مجموعة عامة - ويحتوي Rust بالفعل على حزمة توفرها حرفيًا .


الآن لدينا تحت تصرفنا الكود التالي:


 use generic_array::{GenericArray, ArrayLength}; fn foo<El, Size>(x: GenericArray<El, Size>, y: El) -> Option<usize> where El: Eq, Size: ArrayLength<El>, { // 10000    } 

ماذا نعرف من هذا الكود؟ تأخذ هذه الوظيفة مجموعة من بعض الحجم الثابت ، تنعكس في نوعها (ويتم تجميعها بشكل مستقل لكل حجم من هذا القبيل). حتى الآن ، لا يتغير هذا كثيرًا - في النهاية ، نفس الضمانات بالضبط ، ليس فقط في مرحلة المونورفورم ، ولكن في وقت التشغيل ، قدمت النسخة السابقة مع خفض.


ولكن يمكننا أن نذهب أبعد من ذلك.


نوع المستوى الحسابي


ذكرت المقالة الأصلية عدة ضمانات تلقيناها من إدريس ولم نتمكن من الحصول عليها من أي شخص آخر. واحد منهم - وربما أبسط واحد ، لأنه حتى لا تحتاج إلى كتابة دليل كامل أو اختبار كامل ، ولكن فقط لتحديد النوع قليلاً - يقول أن قيمة الإرجاع ، إذا كانت موجودة (على سبيل المثال ، إذا لم يكن هناك Nothing ) ، يضمن عدم تجاوز طول قائمة المدخلات.

يبدو أن الشرط الضروري لمثل هذا الضمان هو وجود أنواع تابعة ، حسناً ، أو على الأقل نوع من التشابه ، وسيكون من الغريب أن نتوقع شيئًا كهذا من رست ، أليس كذلك؟


يجتمع - typenum . مع ذلك ، يمكن تصور وظيفتنا مثل هذا:


 use generic_array::{ArrayLength, GenericArray}; use typenum::{IsLess, Unsigned, B1}; trait UnsignedLessThan<T> { fn as_usize(&self) -> usize; } impl<Less, More> UnsignedLessThan<More> for Less where Less: IsLess<More, Output = B1>, Less: Unsigned, { fn as_usize(&self) -> usize { <Self as Unsigned>::USIZE } } fn foo<El, Size>(x: GenericArray<El, Size>, y: El) -> Option<Box<dyn UnsignedLessThan<Size>>> where El: Eq, Size: ArrayLength<El>, { // 10000    } 

"ماذا بحق الجحيم هو هذا السحر الأسود؟!" - أنت تسأل. وسوف تكون على حق بالتأكيد: إن typenum هو ذلك السحر الأسود ، ومحاولات استخدامه على الأقل بطريقة ما بطريقة عقلانية مضاعفة.

ومع ذلك ، فإن توقيع هذه الوظيفة لا لبس فيه.


  • تقبل الدالة صفيفًا من عناصر El ذات الطول Size وعنصر واحد من النوع El.
  • تقوم الدالة بإرجاع قيمة خيار ، والتي ، إذا كانت بعض ،
    • إنه كائن سمة يستند إلى نوع UnsignedLessThan<T> ، والذي يقبل نوع الحجم كمعلمة ؛
    • بدوره ، IsLess<T> تطبيق IsLess<T> UnsignedLessThan<T> على جميع الأنواع التي تقوم بتطبيق Unsigned و IsLess<T> والتي تقوم IsLess<T> بإرجاع B1 ، أي صحيح.

بمعنى آخر ، بهذه الطريقة كتبنا دالة تضمن إرجاع رقم غير سالب (غير موقَّع) أصغر من الحجم الأصلي للصفيف (أو بالأحرى ، فإنها تُرجع كائن السمات هذا ، الذي يجب أن نطلق منه as_usize طريقة as_usize ، مع ضمان إرجاع هذا الرقم) .


هناك بالضبط حيلتان في هذا النهج:


  1. يمكننا أن نفقد بشكل ملحوظ في الأداء. إذا كانت هذه الوظيفة فجأة ، لسبب ما ، في الجزء "الساخن" من البرنامج ، فقد تكون الحاجة المستمرة للمكالمات الديناميكية واحدة من أبطأ العمليات. ومع ذلك ، قد لا يكون هذا العيب كبيرًا كما يبدو ، ولكن هناك عائقًا آخر:
  2. لكي يتم ترجمة هذه الوظيفة بشكل صحيح ، سنحتاج إما إلى كتابة إثبات صحة عملها أو "خداع" نظام الكتابة من خلالها بطريقة unsafe . الأول معقد للغاية بالنسبة لمقال الجمعة ، أما الثاني فهو مجرد عملية احتيال.

استنتاج


بطبيعة الحال ، في الممارسة العملية ، في مثل هذه الحالات ، سيتم استخدام إما التنفيذ الثاني (تلقي شريحة من نوع تعسفي) أو تنفيذ تحت المفسد (تلقي كائن قابل للتكرار). من شبه المؤكد أن جميع الحجج اللاحقة ليست لها مصلحة عملية وتخدم فقط كممارسة في العمل مع نظام الكتابة.


ومع ذلك ، فإن حقيقة أن نظام النوع Rust يمكن أن يكون قادرًا على محاكاة إحدى ميزات نظام نوع Idris الأقوى بشكل واضح ، في رأيي ، مؤشر تمامًا.

Source: https://habr.com/ru/post/ar468145/


All Articles