اختبارات مقابل أنواع - إصدار الصدأ

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


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

"هنا C وهناك Haskell ، وماذا عن Rust؟" - كان هذا هو السؤال الأول في المناقشة التالية. الرد هنا.


لنذكر أولاً المهمة:


في حالة وجود قائمة بالقيم والقيمة ، قم بإرجاع فهرس القيمة في القائمة أو يدل على أنها غير موجودة في القائمة.

إذا كان شخص ما لا يريد أن لا يقرأ هذا كله ، فيتم توفير أمثلة الكود في ملعب Rust .
خلاف ذلك ، لنبدأ!



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


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

ماذا نعرف عن هذه الوظيفة؟ حسنًا ، في الحقيقة - ليس كثيرًا. بالطبع ، يعد 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, { // Implementation elided } 

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


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


 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); } 

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


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

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


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

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


الآن ، سوف نتحقق من هذا:


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

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


كما كان من قبل ، ولكن أكثر صرامة قليلا


لكن الآن ، ربما نحتاج إلى مزيد من الضمانات. أو نرغب في العمل على الحزمة (وبالتالي لا يمكننا استخدام Vec ) ، لكننا بحاجة إلى تعميم التعليمات البرمجية الخاصة بنا لكل حجم صفيف ممكن. أو نريد تجميع الوظيفة المحسنة لكل حجم صفيف ملموس.


على أي حال ، نحن بحاجة إلى مجموعة عامة - وهناك صندوق في Rust ، يوفر ذلك بالضبط .


الآن ، إليك رمزنا:


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

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


ولكن يمكننا الحصول على مزيد من.


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


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


يبدو أن الأنواع التابعة - أو شيء من هذا القبيل - ضرورية لمثل هذا الضمان ، ولا يمكننا الحصول على نفس الشيء من Rust ، أليس كذلك؟


تلبية 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>, { // Implementation elided } 

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

ولكن هذا التوقيع وظيفة ملموسة إلى حد ما.


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

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


أستطيع الآن أن أتحدث عن اثنين من المحاذير الرئيسية:


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

استنتاج


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


على أي حال ، فإن حقيقة أن نظام النوع Rust يمكن أن يحاكي الميزة من نظام نوع Idris الأقوى ، بالنسبة لي ، أمر مثير للإعجاب بحد ذاته.

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


All Articles