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

الأنماط Patterns واستخداماتها وقابليتها للدحض Refutability في لغة رست


Naser Dakhel

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

أذرع تعبير match

كما تحدثنا سابقًا في الفصل التعدادات enums في لغة رست، يمكننا استخدام الأنماط في أذرع arms تعبير match، ويُعرَّف التعبير match بالكلمة المفتاحية match، تليها قيمة للتطابق معها وواحد أو أكثر من أذرع المطابقة التي تتألف من نمط وتعبير يُنفذ إذا طابقت القيمة نمط الذراع على النحو التالي:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

على سبيل المثال إليك تعبير match من الشيفرة 5 (من الفصل المذكور آنفًا) الذي يطابق القيمة Option<i32>‎ في المتغير x:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

الأنماط في تعبير match السابق، هما: None و Some(i)‎ على يسار كل سهم.

أحد مُتطلبات تعبير match هو ضرورة كونه شاملًا لجميع الحالات، أي ينبغي أخذ جميع القيم المُحتملة بالحسبان في تعبير match. أحد الطرق لضمان تغطية شاملة الاحتمالات هي وجود نمط مطابقة مع الكل Catchall pattern للذراع الأخير، فلا يمكن مثلًا فشل تطابق اسم متغير لأي قيمة إطلاقًا وبذلك يغطي كل حالة متبقية.

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

تعابير if let الشرطية

تحدثنا في الفصل التعدادات enums في لغة رست عن كيفية استخدام تعابير if let بصورةٍ أساسية مثل طريقة مختصرة لكتابة مكافئ التعبير match، بحيث يطابق حالةً واحدةً فقط، ويمكن أن يكون التعبير if let مترافقًا مع else اختياريًا، بحيث يحتوي على شيفرة برمجية تُنفّذ في حال لم يتطابق النمط في if let.

تبيّن الشيفرة 1 أنه من الممكن مزج ومطابقة تعابير if let و else if و else if let، لإعطاء مرونة أكثر من استخدام التعبير match الذي يقارن قيمةً واحدة مع الأنماط. إضافةً إلى ذلك، لا تتطلب رست أن تكون الأذرع في سلسلة if let أو else if أو else if let متعلقة ببعضها بعضًا.

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

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

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

[الشيفرة 1: استخدام if let و else if و else if let و else بنفس الوقت]

يُستخدم اللون المفضّل للمستخدم لونًا للخلفية إذا حدده المستخدم، وفي حال لم يُحدد لونه المفضل وكان اليوم خميس فسيكون لون الخلفية أخضر؛ وإذا حدّد المستخدم عمره في سلسلة نصية string، يمكننا تحليلها إلى رقم بنجاح، ويكون اللون إما برتقاليًا أو بنفسجيًا اعتمادًا على قيمة هذا الرقم؛ وأخيرًا إذا لم تنطبق أي من هذه الشروط سيكون لون الخلفية أزرق.

يسمح لنا هذا الهيكل الشرطي بدعم المتطلبات المعقدة، إذ نطبع في هذا المثال باستخدام القيم المضمّنة في الشيفرة ما يلي:

Using purple as the background color

يمكنك ملاحظة أن التعبير if let تسبب بحصولنا على متغير مخفي shadowed variable بالطريقة ذاتها التي تسببت بها أذرع التعبير match؛ إذ يُنشئ السطر if let Ok(age) = age متغيرًا مخفيًا جديد يدعى age يحتوي على القيمة داخل المتغاير Ok، وهذا يعني أنه يجب إضافة الشرط if age > 30 داخل الكتلة؛ إذ لا يمكننا جمع الشَرطين في if let ok(age) = age && age > 30، والقيمة الخفية age التي نريد مقارنتها مع 30 غير صالحة حتى يبدأ النطاق scope الجديد بالقوس المعقوص curly bracket.

الجانب السلبي في استخدامنا لتعبير if let هو أن المصرّف لا يتحقق من شمولية الحالات مثلما يفعل تعبير match. إذا تخلّينا عن كتلة else الأخيرة وبالتالي فوّتنا معالجة بعض الحالات، لن ينبهنا المصرّف على احتمالية وجود خطأ منطقي.

حلقات while let الشرطية

تسمح الحلقة الشرطية while let بتنفيذ حلقة while طالما لا يزال النمط مُطابقًا على نحوٍ مشابه لبُنية if let. كتبنا في الشيفرة 2 حلقة while let تستخدم شعاعًا مثل مكدّس stack وتطبع القيم في الشعاع بالترتيب العكسي لإدخالها.

fn main() {
    let mut stack = Vec::new();

    stack.push(1);
    stack.push(2);
    stack.push(3);

    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
}

[الشيفرة 2: استخدام حلقة while let لطباعة القيم طالما يُعيد استدعاء stack.pop()‎ المتغاير Some]

يطبع المثال السابق القيمة 3 ثم 2 ثم 1، إذ يأخذ التابع pop آخر عنصر في الشعاع vector ويُعيد Some(value)‎، ويعيد القيمة None إذا كان الشعاع فارغًا. يستمر تنفيذ الحلقة while والشيفرة البرمجية داخل كتلتها طالما يُعيد استدعاء pop القيمة Some، وعندما يعيد استدعاء pop القيمة None تتوقف الحلقة، ويمكننا استخدام while let لإزالة pop off كل عنصر خارج المكدّس.

حلقات for التكرارية

القيمة التي تتبع الكلمة المفتاحية for في حلقة for هي النمط، فعلى سبيل المثال النمط في for x in y هو x. توضح الشيفرة 3 كيفية استخدام النمط في حلقة for لتفكيك destructure أو تجزئة الصف tuple إلى جزء من حلقة for.

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (index, value) in v.iter().enumerate() {
        println!("{} is at index {}", value, index);
    }
}

[الشيفرة 3: استخدام نمط في حلقة for لتفكيك صف]

تطبع الشيفرة 3 ما يلي:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

نُعدل مكرّرًا iterator باستخدام التابع enumerate بحيث يعطينا قيمةً ودليلًا index لهذه القيمة ضمن صف tuple، بحيث تكون القيمة الأولى هي الصف ('‎0, 'a). عندما تطابق هذه القيمة النمط (index, value) ستكون index هي 0 وستكون value هي a، مما سيتسبب بطباعة السطر الأول من الخرج.

تعليمات let

تحدثنا سابقًا عن استخدام الأنماط مع match و if let فقط، إلا أننا استخدمنا الأنماط في أماكن أُخرى أيضًا مثل تعليمة let. ألقِ نظرةً على عملية إسناد المتغير التالية باستخدام let:

#![allow(unused)]
fn main() {
let x = 5;
}

اِستخدمت الأنماط في كلّ مرة استخدمت فيها تعليمة let بالشكل السابق دون معرفتك لذلك. تكون تعليمة let بالشكل التالي:

let PATTERN = EXPRESSION;

يشكّل اسم المتغير في التعليمات المشابهة لتعليمة let x = 5;‎ -مع اسم المتغير في فتحة PATTERN- نوعًا بسيطًا من الأنماط. تقارن رست التعابير مع الأنماط وتُسند أي اسم تجده، أي تتألف التعليمة let x = 5‎;‎ من نمط هو x يعني "اربط ما يتطابق هنا مع المتغير x"، ولأن الاسم x يمثّل كامل النمط، فإن هذا النمط يعني "اربط كل شيء مع المتغير x مهما تكُن القيمة".

لملاحظة مطابقة النمط في let بوضوح أكبر، جرّب الشيفرة 4 التي تستخدم نمطًا مع let لتفكيك الصف.

    let (x, y, z) = (1, 2, 3);

[الشيفرة 4: استخدام نمط لتفكيك الصف وإنشاء ثلاثة متغيرات بخطوة واحدة]

طابقنا الصف مع النمط هنا، إذ تُقارن رست القيمة (3, 2, 1) مع النمط (x, y, z) وترى أن القيمة تطابق النمط فعلًا، لذلك تربط رست 1 إلى xو 2 إلى y و 3 إلى z. يمكن عدّ نمط الصف هذا بمثابة تضمين ثلاثة أنماط متغيرات مفردة داخله.

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

   let (x, y) = (1, 2, 3);

[الشيفرة 5: بناء خاطئ لنمط لا تطابق متغيراته عدد العناصر الموجودة في الصف]

ستتسبب محاولة تصريف الشيفرة السابقة بالخطأ التالي:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` due to previous error

يمكننا تجاهل قيمة أو أكثر في الصف لتصحيح الخطأ وذلك باستخدام _ أو .. كما سنرى لاحقًا في قسم "تجاهل القيم في النمط". إذا كانت المشكلة هي وجود عدة متغيرات في النمط فإن الحل يكمن بجعل الأنواع تتطابق عن طريق إزالة المتغيرات حتى يتساوى عدد المتغيرات وعدد العناصر في الصف.

معاملات الدالة Function Parameters

يمكن لمعاملات الدالة أن تمثّل أنماطًا. ينبغي أن تكون الشيفرة 6 مألوفةً بالنسبة لك، إذ نصرّح فيها عن دالة اسمها foo تقبل معاملًا واحدًا اسمه x من النوع i32.

fn foo(x: i32) {
    // تُكتب الشيفرة البرمجية هنا
}

[الشيفرة 6: بصمة دالة function signature تستخدم الأنماط في معاملاتها]

يشكّل الجزء x نمطًا. يمكننا مطابقة الصف في وسيط الدالة مع النمط كما فعلنا مع let. تجزِّء الشيفرة 7 القيم الموجودة في الصف عندما نمررها إلى الدالة.

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

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

[الشيفرة 7: دالة تفكك الصف مع معاملاتها]

تطبع الشيفرة السابقة: Current location: (3, 5)‎، إذ تطابق القيم ‎&(3, 5)‎ النمط ‎&(x, y)‎، لذا فإن قيمة x هي 3 وقيمة y هي 5.

يمكننا أيضًا استخدام الأنماط في قوائم معاملات التغليف closure parameter lists بطريقة قوائم معاملات الدالة function parameter lists ذاتها، لأن المغلفات مشابهة للدالات كما رأينا سابقًا في الفصل المغلفات closures في لغة رست.

رأينا بحلول هذه النقطة عدة طرق لاستخدام الأنماط، إلا أن الأنماط لا تعمل بالطريقة ذاتها في كل مكان تُستخدم فيه، إذ يجب أن تكون الأنماط غير قابلة للدحض في بعض الأماكن وفي بعضها الآخر كذلك، وسنتحدث عن هذين المفهومين تاليًا.

قابلية الدحض refutability واحتمالية فشل مطابقة النمط

تأتي الأنماط بشكلين: قابلة للدحض أو النقض refutable وغير قابلة للدحض irrefutable، إذ تُدعى الأنماط التي تطابق أي قيمة تمرر خلالها بالأنماط القابلة للدحض، ومثال على ذلك هو x في التعليمة let x = 5;‎ وذلك لأن المتغير x سيطابق أي شيء وبالتالي لا تفشل المطابقة؛ بينما تُدعى الأنماط التي تفشل في بعض القيم بالأنماط القابلة للدحض، ومثال على ذلك هو Some(x)‎ في التعبير if let Some(x) = a_value لأن النمط Some(x)‎ لن يُطابق إذا كانت القيمة في المتغير a_value هي None عوضًا عن Some.

تقبل معاملات الدالة وتعليمات let وحلقات for فقط الأنماط غير القابلة للدحض لأن البرنامج لا يستطيع عمل أي شيء مفيد عندما لا تتطابق القيم. يقبل التعبيران if let و while let الأنماط القابلة للدحض وغير القابلة للدحض، إلا أنّ المصرّف يحذّر من استخدام الأنماط غير القابلة للدحض، لأنها -بحسب تعريفها- ليست معدّة لتتعامل مع فشل محتمل، إذ تتمثّل الوظيفة الشرطية بقدرتها على التصرف بصورةٍ مختلفة اعتمادًا على النجاح أو الفشل.

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

لنتابع مثالّا لما قد يحصل عندما نجرب استخدام نمط قابل للدحض عندما تتطلب رست نمطّا غير قابل للدحض -والعكس صحيح- إذ تبيّن الشيفرة 8 تعليمة let إلا أن النمط الذي حددناه هو Some(x)‎ وهو نمط قابل للدحض، ولن تُصرَّف الشيفرة كما هو متوقع.

let Some(x) = some_option_value;

[الشيفرة 8: محاولة استخدام نمط قابل للدحض مع let]

ستفشل مطابقة النمط في Some(x)‎ إذا كانت القيمة في some_option_value هي None أي أن النمط هو قابل للدحض، ولكن تقبل تعليمة let فقط الأنماط غير القابلة للدحض لأنه لا توجد قيمة صالحة تستطيع الشيفرة استخدامها مع قيمة None. تنبّهنا رست عند استخدام قيمة قابلة للدحض عندما يتطلب الأمر وجود قيمة غير قابلة للدحض وقت التصريف:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding: `None` not covered
 --> src/main.rs:3:9
  |
3 |     let Some(x) = some_option_value;
  |         ^^^^^^^ pattern `None` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
note: `Option<i32>` defined here
 --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:518:1
  |
  = note: 
/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:522:5: not covered
  = note: the matched value is of type `Option<i32>`
help: you might want to use `if let` to ignore the variant that isn't matched
  |
3 |     let x = if let Some(x) = some_option_value { x } else { todo!() };
  |     ++++++++++                                 ++++++++++++++++++++++
help: alternatively, you might want to use let else to handle the variant that isn't matched
  |
3 |     let Some(x) = some_option_value else { todo!() };
  |                                     ++++++++++++++++

For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` due to previous error

تُعطينا رست الخطأ التصريفي السابق، وذلك بسبب عدم تغطيتنا لكل القيم الممكنة مع النمط Some(x)‎، ولن نستطيع فعل ذلك حتى لو أردنا.

يمكننا إصلاح المشكلة في حال وجود نمط قابل للدحض يحل مكان نمط غير قابل للدحض عن طريق تغيير الشيفرة التي تستخدم النمط؛ فبدلًا من استخدام let نستخدم if let، وهكذا إذا لم يُطابق النمط تتخطى الشيفرة تلك الشيفرة الموجودة في القوسين المعقوصين وتستمر بذلك صلاحية الشيفرة. تبيّن الشيفرة 9 كيفية إصلاح الخطأ في الشيفرة 8.

    if let Some(x) = some_option_value {
        println!("{}", x);
    }

[الشيفرة 9: استخدام if let وكتلة تحتوي على أنماط قابلة للدحض بدلًا من let]

سُمح للشيفرة السابقة بالتصريف، فهذه الشيفرة صالحة، على الرغم من أنه لا يمكن استخدام نمط غير قابل للدحض دون رسالة خطأ. إذا أعطينا if let نمطًا يطابق دومًا مثل x كما في الشيفرة 10، سيمنحنا المصرّف تنبيهًا.

fn main() {
    if let x = 5 {
        println!("{}", x);
    };
}

[الشيفرة 10: محاولة استخدام نمط غير قابل للدحض مع if let]

تشتكي رست من عدم منطقيّة استخدام if let مع نمط غير قابل للدحض:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `if let` pattern
 --> src/main.rs:2:8
  |
2 |     if let x = 5 {
  |        ^^^^^^^^^
  |
  = note: this pattern will always match, so the `if let` is useless
  = help: consider replacing the `if let` with a `let`
  = note: `#[warn(irrefutable_let_patterns)]` on by default

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

يجب أن تستخدم مطابقة الأذرع match arms الأنماط القابلة للدحض لهذا السبب ما عدا الذراع الأخير، الذي يجب أن يطابق أي قيمة متبقية من النمط غير القابل للدحض. تسمح رست باستخدام نمط غير قابل للدحض في match باستخدام ذراع واحد فقط، ولكن الصياغة هذه ليست مفيدة ويمكن استبدالها بتعليمة let أبسط.

بعدما عرفنا أماكن استخدام الأنماط والفرق بين الأنماط القابلة للدحض وغير القابلة للدحض، دعنا نكمل طريقة الصياغة syntax التي يمكن استخدامها لإنشاء الأنماط.

ترجمة -وبتصرف- لقسم من الفصل Patterns and Matching من كتاب 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.


×
×
  • أضف...