كود كتابة الكود: مقدمة لنظرية وممارسة البرمجة الوصفية الحديثة
نشرت: 2022-07-22عندما أفكر في أفضل طريقة لشرح وحدات الماكرو ، أتذكر برنامج Python الذي كتبته عندما بدأت البرمجة لأول مرة. لم أستطع تنظيمه بالطريقة التي أردت. اضطررت إلى استدعاء عدد من الوظائف المختلفة قليلاً ، وأصبح الرمز مرهقًا. ما كنت أبحث عنه - على الرغم من أنني لم أكن أعرف ذلك في ذلك الوقت - كان البرمجة الوصفية .
metaprogramming (اسم)
أي تقنية يمكن للبرنامج من خلالها التعامل مع الكود على أنها بيانات.
يمكننا إنشاء مثال يوضح نفس المشكلات التي واجهتها مع مشروع Python الخاص بي من خلال تخيل أننا نبني النهاية الخلفية لتطبيق لأصحاب الحيوانات الأليفة. باستخدام الأدوات الموجودة في المكتبة ، pet_sdk
، نكتب لغة Python لمساعدة أصحاب الحيوانات الأليفة في شراء طعام القطط:
بعد التأكد من عمل الكود ، ننتقل إلى تطبيق نفس المنطق لنوعين آخرين من الحيوانات الأليفة (الطيور والكلاب). نضيف أيضًا ميزة لحجز مواعيد الطبيب البيطري:
سيكون من الجيد تكثيف المنطق المتكرر لـ Snippet 2 في حلقة ، لذلك شرعنا في إعادة كتابة الكود. سرعان ما ندرك أنه نظرًا لأنه يتم تسمية كل وظيفة بشكل مختلف ، لا يمكننا تحديد أي وظيفة (على سبيل المثال ، book_bird_appointment
، أو book_cat_appointment
) نطلبها في حلقةنا:
دعنا نتخيل إصدارًا مشحونًا بشاحن توربيني من Python يمكننا من خلاله كتابة البرامج التي تُنشئ تلقائيًا الكود النهائي الذي نريده - وهو برنامج يمكننا من خلاله معالجة برنامجنا بمرونة وسهولة وسلاسة كما لو كان قائمة أو بيانات في ملف أو أي نوع البيانات الشائعة الأخرى أو إدخال البرنامج:
هذا مثال لماكرو ، متاح بلغات مثل Rust أو Julia أو C ، على سبيل المثال لا الحصر - ولكن ليس Python.
هذا السيناريو هو مثال رائع على كيف يمكن أن يكون من المفيد كتابة برنامج قادر على تعديل ومعالجة الكود الخاص به. هذا هو بالضبط رسم وحدات الماكرو ، وهو واحد من العديد من الإجابات على سؤال أكبر: كيف يمكننا الحصول على برنامج لفحص الكود الخاص به ، والتعامل معه على أنه بيانات ، ثم التصرف بناءً على هذا الاستبطان؟
بشكل عام ، تندرج جميع التقنيات التي يمكنها تحقيق مثل هذا الاستبطان ضمن المصطلح الشامل "البرمجة الوصفية". Metaprogramming هي حقل فرعي غني في تصميم لغة البرمجة ، ويمكن إرجاعها إلى مفهوم واحد مهم: الكود كبيانات.
انعكاس: دفاعًا عن بايثون
قد تشير إلى أنه على الرغم من أن Python قد لا توفر دعمًا ماكروًا ، إلا أنها توفر الكثير من الطرق الأخرى لكتابة هذا الرمز. على سبيل المثال ، نستخدم هنا طريقة isinstance()
لتحديد الفئة التي يعد المتغير animal
الخاص بنا مثالًا لها واستدعاء الوظيفة المناسبة:
نسمي هذا النوع من انعكاس البرمجة الوصفية ، وسنعود إليه لاحقًا. لا يزال رمز Snippet 5 مرهقًا بعض الشيء ولكن يسهل على المبرمج كتابته أكثر من Snippet 2 ، حيث كررنا المنطق لكل حيوان مدرج في القائمة.
تحدي
باستخدام طريقة getattr
، قم بتعديل الكود السابق لاستدعاء order_*_food
and book_*_appointment
ديناميكيًا. يمكن القول إن هذا يجعل الكود أقل قابلية للقراءة ، ولكن إذا كنت تعرف Python جيدًا ، فمن الجدير التفكير في كيفية استخدام getattr
بدلاً من وظيفة isinstance
، وتبسيط الكود.
اللواط: أهمية اللثغة
بعض لغات البرمجة ، مثل Lisp ، تأخذ مفهوم metaprogramming إلى مستوى آخر عبر homoiconicity .
اللواط (اسم)
خاصية لغة البرمجة حيث لا يوجد تمييز بين الكود والبيانات التي يعمل عليها البرنامج.
Lisp ، التي تم إنشاؤها في عام 1958 ، هي أقدم لغة homoiconic وثاني أقدم لغة برمجة عالية المستوى. اشتُق اسم LIS من "LISt Processor" ، وكان بمثابة ثورة في الحوسبة التي شكلت بعمق كيفية استخدام أجهزة الكمبيوتر وبرمجتها. من الصعب المبالغة في تقدير مدى تأثير ليسب بشكل أساسي ومميز على البرمجة.
Emacs مكتوب بلغة Lisp ، وهي لغة الكمبيوتر الوحيدة الجميلة. نيل ستيفنسون
تم إنشاء Lisp بعد عام واحد فقط من FORTRAN ، في عصر البطاقات المثقوبة وأجهزة الكمبيوتر العسكرية التي تملأ الغرفة. ومع ذلك ، لا يزال المبرمجون يستخدمون Lisp اليوم لكتابة تطبيقات جديدة وحديثة. كان مبتكر ليسب الأساسي ، جون مكارثي ، رائدًا في مجال الذكاء الاصطناعي. لسنوات عديدة ، كانت Lisp هي لغة الذكاء الاصطناعي ، حيث يقدر الباحثون القدرة على إعادة كتابة التعليمات البرمجية الخاصة بهم ديناميكيًا. تتركز أبحاث الذكاء الاصطناعي اليوم حول الشبكات العصبية والنماذج الإحصائية المعقدة ، بدلاً من هذا النوع من كود التوليد المنطقي. ومع ذلك ، فإن البحث الذي تم إجراؤه على الذكاء الاصطناعي باستخدام Lisp - خاصة البحث الذي تم إجراؤه في الستينيات والسبعينيات في MIT و Stanford - خلق المجال كما نعرفه ، ويستمر تأثيره الهائل.
عرض مجيء ليسب المبرمجين الأوائل للإمكانيات الحسابية العملية لأشياء مثل التكرار ووظائف الترتيب الأعلى والقوائم المرتبطة لأول مرة. كما أظهر قوة لغة البرمجة المبنية على أفكار حساب لامدا.
أثارت هذه المفاهيم انفجارًا في تصميم لغات البرمجة ، وكما قال Edsger Dijkstra ، أحد أعظم الأسماء في علوم الكمبيوتر ، " [...] ساعد عددًا من زملائنا البشر الأكثر موهبة في التفكير في أفكار كانت مستحيلة سابقًا."
يوضح هذا المثال برنامج Lisp البسيط (وما يعادله في بناء جملة Python الأكثر شيوعًا) الذي يحدد دالة "عاملة" تحسب بشكل متكرر عامل إدخالها وتستدعي تلك الوظيفة مع الإدخال "7":
لثغة | بايثون |
---|---|
( defun factorial ( n ) ( if ( = n 1 ) 1 ( * n ( factorial ( - n 1 ))))) ( print ( factorial 7 )) | |
كود كبيانات
على الرغم من كونها واحدة من أكثر ابتكارات Lisp تأثيرًا وتأثيرًا ، إلا أن المثلية ، على عكس التكرار والعديد من المفاهيم الأخرى التي ابتكرها Lisp ، لم تصل إلى معظم لغات البرمجة الحالية.
يقارن الجدول التالي الدوال المتجانسة التي تُرجع التعليمات البرمجية في كل من Julia و Lisp. Julia هي لغة homoiconic تشبه ، من نواح كثيرة ، اللغات عالية المستوى التي قد تكون على دراية بها (مثل Python و Ruby).
الجزء الأساسي في بناء الجملة في كل مثال هو حرف الاقتباس . تستخدم جوليا :
(نقطتان) للاقتباس ، بينما تستخدم Lisp '
(اقتباس مفرد):
جوليا | لثغة |
---|---|
function function_that_returns_code() return :(x + 1 ) end | |
في كلا المثالين ، فإن الاقتباس الموجود بجانب التعبير الرئيسي ( (x + 1)
أو (+ x 1)
) يحولها من رمز تم تقييمه مباشرة إلى تعبير مجردة يمكننا معالجته. تقوم الوظيفة بإرجاع رمز - وليس سلسلة أو بيانات. إذا كان علينا استدعاء وظيفتنا وكتابة print(function_that_returns_code())
، فإن جوليا ستطبع الكود المرتب على شكل x+1
(والمكافئ صحيح لـ Lisp). على العكس من ذلك ، بدون :
(أو '
Lisp) ، سنحصل على خطأ مفاده أن x
لم يتم تعريفه.
دعنا نعود إلى مثال جوليا الخاص بنا ونقوم بتوسيعه:
يمكن استخدام وظيفة eval
لتشغيل الكود الذي ننشئه من مكان آخر في البرنامج. لاحظ أن القيمة المطبوعة تستند إلى تعريف المتغير x
. إذا حاولنا eval
الكود الذي تم إنشاؤه في سياق لم يتم فيه تعريف x
، فسنحصل على خطأ.
تعد Homoiconicity نوعًا قويًا من البرمجة الوصفية ، وهي قادرة على إطلاق العنان لنماذج برمجة جديدة ومعقدة يمكن للبرامج أن تتكيف معها بشكل سريع ، وتولد رمزًا يناسب المشكلات الخاصة بالمجال أو تنسيقات البيانات الجديدة التي يتم مواجهتها.
خذ حالة WolframAlpha ، حيث يمكن أن تنشئ لغة Wolfram homoiconic كودًا للتكيف مع مجموعة لا تصدق من المشاكل. يمكنك أن تسأل WolframAlpha ، "ما هو الناتج المحلي الإجمالي لمدينة نيويورك مقسومًا على سكان أندورا؟" وبشكل ملحوظ تلقي استجابة منطقية.
يبدو من غير المحتمل أن يفكر أي شخص في تضمين هذا الحساب الغامض وغير المجدي في قاعدة بيانات ، لكن ولفرام يستخدم البرمجة الوصفية ورسمًا بيانيًا للمعرفة الوجودية لكتابة رمز سريع للإجابة على هذا السؤال.
من المهم أن نفهم المرونة والقوة التي توفرها Lisp واللغات homoiconic الأخرى. قبل أن نتعمق أكثر ، دعنا نفكر في بعض خيارات البرمجة الوصفية المتاحة لك:
تعريف | أمثلة | ملحوظات | |
---|---|---|---|
اللواط | خاصية لغة يكون فيها الرمز عبارة عن بيانات "من الدرجة الأولى". نظرًا لعدم وجود فصل بين الكود والبيانات ، يمكن استخدام الاثنين بالتبادل. |
| هنا ، تتضمن Lisp لغات أخرى في عائلة Lisp ، مثل Scheme و Racket و Clojure. |
وحدات الماكرو | عبارة أو وظيفة أو تعبير يأخذ الكود كمدخل ويعيد الكود كمخرج. |
| (راجع الملاحظة التالية حول وحدات الماكرو الخاصة بـ C.) |
توجيهات المعالج المسبق (أو المترجم المسبق) | النظام الذي يأخذ البرنامج كمدخل ، وبناءً على العبارات المضمنة في الكود ، يقوم بإرجاع نسخة معدلة من البرنامج كمخرجات. |
| يتم تنفيذ وحدات الماكرو الخاصة بـ C باستخدام نظام المعالج المسبق لـ C ، ولكن كلاهما مفهومان منفصلان. يتمثل الاختلاف المفاهيمي الرئيسي بين وحدات الماكرو الخاصة بـ C (التي نستخدم فيها التوجيه #define preprocessor) وأشكال أخرى من توجيهات المعالج المسبق لـ C (على سبيل المثال ، #if و #ifndef ) في أننا نستخدم وحدات الماكرو لإنشاء رمز أثناء استخدام غير #define توجيهات المعالج المسبق لترجمة التعليمات البرمجية الأخرى بشكل مشروط. الاثنان مرتبطان ارتباطًا وثيقًا بلغة C وفي بعض اللغات الأخرى ، لكنهما نوعان مختلفان من البرمجة الوصفية. |
انعكاس | قدرة البرنامج على فحص وتعديل واستبطان الكود الخاص به. |
| يمكن أن يحدث الانعكاس في وقت الترجمة أو في وقت التشغيل. |
علم الوراثة | القدرة على كتابة التعليمات البرمجية الصالحة لعدد من الأنواع المختلفة أو التي يمكن استخدامها في سياقات متعددة ولكن مخزنة في مكان واحد. يمكننا تحديد السياقات التي تكون فيها الشفرة صالحة إما بشكل صريح أو ضمني. | الأدوية ذات النمط النموذجي:
تعدد الأشكال البارامترية:
| تعد البرمجة العامة موضوعًا أوسع من البرمجة الوصفية العامة ، والخط الفاصل بينهما غير محدد جيدًا. من وجهة نظر هذا المؤلف ، يُحسب نظام الكتابة البارامترية فقط على أنه Metaprogramming إذا كان بلغة مكتوبة بشكل ثابت. |
دعنا نلقي نظرة على بعض الأمثلة العملية على homoiconicity ، وحدات الماكرو ، توجيهات المعالج المسبق ، الانعكاس ، والأدوية المكتوبة بلغات برمجة مختلفة:
أصبحت وحدات الماكرو (مثل تلك الموجودة في Snippet 11) شائعة مرة أخرى في جيل جديد من لغات البرمجة. لتطويرها بنجاح ، يجب أن نأخذ في الاعتبار موضوعًا رئيسيًا: النظافة.
وحدات الماكرو الصحية وغير الصحية
ماذا يعني أن تكون الشفرة "صحية" أو "غير صحية"؟ للتوضيح ، دعنا ننظر إلى ماكرو Rust ، تم إنشاء مثيل له بواسطة macro_rules!
وظيفة. كما يوحي الاسم ، فإن macro_rules!
يولد رمزًا بناءً على القواعد التي نحددها. في هذه الحالة ، قمنا بتسمية الماكرو الخاص بنا my_macro
، والقاعدة هي "إنشاء سطر من التعليمات البرمجية let x = $n
" ، حيث n
هو الإدخال الخاص بنا:
عندما نقوم بتوسيع الماكرو (تشغيل ماكرو لاستبدال استدعائه بالرمز الذي ينشئه) ، نتوقع الحصول على ما يلي:
على ما يبدو ، أعاد الماكرو الخاص بنا تعريف المتغير x
ليساوي 3 ، لذلك قد نتوقع بشكل معقول أن يقوم البرنامج بطباعة 3
. في الواقع ، إنها تطبع 5
! متفاجئ؟ في Rust، macro_rules!
صحية فيما يتعلق بالمعرفات ، لذلك لن "تلتقط" المعرفات خارج نطاقها. في هذه الحالة ، كان المعرف هو x
. لو تم التقاطها بواسطة الماكرو ، لكانت تساوي 3.
النظافة (اسم)
خاصية تضمن أن توسع الماكرو لن يلتقط المعرفات أو الحالات الأخرى من خارج نطاق الماكرو. تسمى وحدات الماكرو وأنظمة الماكرو التي لا توفر هذه الخاصية غير الصحية .
تعد النظافة في وحدات الماكرو موضوعًا مثيرًا للجدل إلى حد ما بين المطورين. يصر المؤيدون على أنه بدون النظافة ، من السهل جدًا تعديل سلوك الكود الخاص بك عن طريق الصدفة. تخيل وحدة ماكرو أكثر تعقيدًا بشكل ملحوظ من Snippet 13 المستخدمة في التعليمات البرمجية المعقدة مع العديد من المتغيرات والمعرفات الأخرى. ماذا لو استخدم هذا الماكرو أحد المتغيرات نفسها مثل التعليمات البرمجية الخاصة بك - ولم تلاحظ ذلك؟
ليس من غير المعتاد أن يستخدم المطور ماكروًا من مكتبة خارجية دون قراءة التعليمات البرمجية المصدر. هذا شائع بشكل خاص في اللغات الأحدث التي تقدم دعمًا ماكروًا (على سبيل المثال ، Rust و Julia):
يلتقط هذا الماكرو غير الصحي في C website
المعرف ويغير قيمته. بالطبع ، التقاط المعرف ليس ضارًا. إنها مجرد نتيجة عرضية لاستخدام وحدات الماكرو.
لذا ، وحدات الماكرو الصحية جيدة ، ووحدات الماكرو غير الصحية سيئة ، أليس كذلك؟ لسوء الحظ ، الأمر ليس بهذه البساطة. هناك حجة قوية يجب إثباتها بأن وحدات الماكرو الصحية تقيدنا. في بعض الأحيان ، يكون التقاط المعرف مفيدًا. دعنا نعيد زيارة Snippet 2 ، حيث نستخدم pet_sdk
لتقديم خدمات لثلاثة أنواع من الحيوانات الأليفة. بدأ الكود الأصلي لدينا على النحو التالي:
ستتذكر أن Snippet 3 كانت محاولة لتكثيف المنطق المتكرر لـ Snippet 2 في حلقة شاملة. ولكن ماذا لو كان كودنا يعتمد على معرفات cats
dogs
، وأردنا أن نكتب شيئًا مثل ما يلي:
يعد Snippet 16 بسيطًا بعض الشيء ، بالطبع ، لكن تخيل حالة نريد فيها ماكرو كتابة 100٪ من جزء معين من التعليمات البرمجية. قد تكون وحدات الماكرو الصحية محدودة في مثل هذه الحالة.
على الرغم من أن الجدل العام الصحي مقابل غير الصحي يمكن أن يكون معقدًا ، إلا أن الخبر السار هو أنه لا يتعين عليك اتخاذ موقف. تحدد اللغة التي تستخدمها ما إذا كانت وحدات الماكرو الخاصة بك ستكون صحية أم غير صحية ، لذا ضع ذلك في الاعتبار عند استخدام وحدات الماكرو.
وحدات الماكرو الحديثة
تواجه وحدات الماكرو بعض الوقت الآن. لفترة طويلة ، تحول تركيز لغات البرمجة الضرورية الحديثة بعيدًا عن وحدات الماكرو كجزء أساسي من وظائفها ، وتجنبها لصالح أنواع أخرى من البرمجة الوصفية.
أخبرتهم اللغات التي كان يتم تدريسها للمبرمجين الجدد في المدارس (على سبيل المثال ، Python و Java) أن كل ما يحتاجون إليه هو التفكير والأدوية.
بمرور الوقت ، عندما أصبحت تلك اللغات الحديثة شائعة ، أصبحت وحدات الماكرو مرتبطة بترهيب بناء جملة المعالجات C و C ++ - إذا كان المبرمجون على دراية بها على الإطلاق.
مع ظهور Rust و Julia ، تحول الاتجاه مرة أخرى إلى وحدات الماكرو. Rust and Julia هما لغتان حديثتان يمكن الوصول إليهما ومستخدمتان على نطاق واسع أعادا تعريف مفهوم وحدات الماكرو ونشرهما ببعض الأفكار الجديدة والمبتكرة. هذا مثير بشكل خاص في Julia ، التي تبدو مهيأة لتحل محل Python و R كلغة سهلة الاستخدام "مضمنة بالبطاريات" متعددة الاستخدامات.
عندما نظرنا لأول مرة إلى pet_sdk
من خلال نظارات "TurboPython" الخاصة بنا ، ما أردناه حقًا هو شيء مثل جوليا. دعنا نعيد كتابة Snippet 2 في Julia ، باستخدام homoiconic لها وبعض أدوات metaprogramming الأخرى التي يقدمها:
لنفصل المقتطف 17:
- نقوم بالتكرار من خلال ثلاث مجموعات. أولها
("cat", :clean_litterbox)
، لذلك تم تعيين المتغيرpet
إلى"cat"
، ويتم تعيين المتغيرcare_fn
للرمز المقتبس:clean_litterbox
. - نستخدم دالة
Meta.parse
لتحويل سلسلة إلىExpression
، حتى نتمكن من تقييمها على أنها رمز. في هذه الحالة ، نريد استخدام قوة استيفاء السلسلة ، حيث يمكننا وضع سلسلة في أخرى ، لتحديد الوظيفة التي يجب استدعاؤها. - نستخدم الدالة
eval
لتشغيل الكود الذي ننشئه.@eval begin… end
هي طريقة أخرى لكتابةeval(...)
لتجنب إعادة كتابة الكود. يوجد داخل كتلة@eval
رمز نقوم بإنشائه بشكل ديناميكي وتشغيله.
يحررنا نظام جوليا البترولي حقًا للتعبير عما نريد بالطريقة التي نريدها. كان بإمكاننا استخدام العديد من الأساليب الأخرى ، بما في ذلك التفكير (مثل Python في Snippet 5). كان بإمكاننا أيضًا كتابة دالة ماكرو تُنشئ صراحةً رمزًا لحيوان معين ، أو يمكننا إنشاء الكود بالكامل كسلسلة واستخدام Meta.parse
أو أي مجموعة من هذه الطرق.
ما وراء جوليا: أنظمة Metaprogramming الحديثة الأخرى
ربما تكون جوليا واحدة من أكثر الأمثلة إثارة للاهتمام وإقناعًا لنظام ماكرو حديث ، لكنها ليست النموذج الوحيد بأي حال من الأحوال. كان Rust أيضًا مفيدًا في جلب وحدات الماكرو أمام المبرمجين مرة أخرى.
في Rust ، تتميز وحدات الماكرو بشكل مركزي أكثر من Julia ، على الرغم من أننا لن نستكشف ذلك بشكل كامل هنا. لمجموعة من الأسباب ، لا يمكنك كتابة Rust الاصطلاحية دون استخدام وحدات الماكرو. ومع ذلك ، في Julia ، يمكنك اختيار تجاهل نظام homoiconic والنظام الكلي تمامًا.
كنتيجة مباشرة لتلك المركزية ، احتضن النظام البيئي Rust حقًا وحدات الماكرو. قام أعضاء المجتمع ببناء بعض المكتبات الرائعة بشكل لا يصدق ، وإثباتات المفهوم ، والميزات باستخدام وحدات الماكرو ، بما في ذلك الأدوات التي يمكنها إجراء تسلسل للبيانات وإلغاء تسلسلها ، وإنشاء SQL تلقائيًا ، أو حتى تحويل التعليقات التوضيحية المتبقية في التعليمات البرمجية إلى لغة برمجة أخرى ، وكلها تم إنشاؤها في رمز في وقت الترجمة.
في حين أن برمجة جوليا الميتابروغرجة قد تكون أكثر تعبيرًا وحرية ، ربما يكون Rust هو أفضل مثال على لغة حديثة ترفع من مستوى البرمجة الوصفية ، حيث إنها مميزة بشكل كبير في جميع أنحاء اللغة.
عين على المستقبل
الآن هو وقت رائع للاهتمام بلغات البرمجة. اليوم ، يمكنني كتابة تطبيق بلغة C ++ وتشغيله في مستعرض ويب أو كتابة تطبيق بلغة JavaScript للتشغيل على سطح المكتب أو الهاتف. لم تكن العوائق التي تحول دون الدخول أقل من أي وقت مضى ، والمبرمجون الجدد لديهم معلومات في متناول أيديهم كما لم يحدث من قبل.
في هذا العالم من اختيار المبرمج والحرية ، لدينا امتياز متزايد لاستخدام اللغات الحديثة الغنية ، والتي تختار الميزات والمفاهيم من تاريخ علوم الكمبيوتر ولغات البرمجة السابقة. من المثير رؤية وحدات الماكرو يتم التقاطها وإزالة الغبار عنها في موجة التطوير هذه. لا أطيق الانتظار لأرى ما سيفعله مطورو الجيل الجديد بينما يقدمهم Rust و Julia إلى وحدات الماكرو. تذكر أن "الشفرة كبيانات" هي أكثر من مجرد عبارة مشهورة. إنها أيديولوجية أساسية يجب وضعها في الاعتبار عند مناقشة البرمجة الوصفية في أي مجتمع عبر الإنترنت أو بيئة أكاديمية.
"الشفرة كبيانات" هي أكثر من مجرد شعار.
كان تاريخ البرمجة الوصفية الممتد على مدار 64 عامًا جزءًا لا يتجزأ من تطوير البرمجة كما نعرفها اليوم. في حين أن الابتكارات والتاريخ الذي اكتشفناه ما هو إلا ركن من أركان ملحمة البرمجة الوصفية ، إلا أنها توضح القوة القوية والمنفعة التي تتميز بها البرمجة الوصفية الحديثة.