البرمجة التصريحية: هل هي شيء حقيقي؟
نشرت: 2022-03-11تعد البرمجة التصريحية ، حاليًا ، النموذج السائد لمجموعة واسعة ومتنوعة من المجالات مثل قواعد البيانات والقوالب وإدارة التكوين.
باختصار ، تتكون البرمجة التصريحية من توجيه البرنامج إلى ما يجب القيام به ، بدلاً من إخباره بكيفية القيام بذلك. في الممارسة العملية ، يستلزم هذا النهج توفير لغة خاصة بالمجال (DSL) للتعبير عما يريده المستخدم ، وحمايته من التركيبات منخفضة المستوى (الحلقات ، الشروط ، التخصيصات) التي تتحقق الحالة النهائية المرغوبة.
في حين أن هذا النموذج يعد تحسينًا ملحوظًا للنهج الإلزامي الذي استبدله ، إلا أنني أؤكد أن البرمجة التصريحية لها قيود وقيود كبيرة أستكشفها في هذه المقالة. علاوة على ذلك ، أقترح نهجًا مزدوجًا يلتقط فوائد البرمجة التصريحية بينما يحل محل قيودها.
كافيت : ظهرت هذه المقالة كنتيجة لصراع شخصي استمر لعدة سنوات مع الأدوات التقريرية. العديد من الادعاءات التي أقدمها هنا لم يتم إثباتها تمامًا ، وبعضها مقدم في ظاهره. سيستغرق النقد المناسب للبرمجة التصريحية وقتًا وجهدًا كبيرًا ، وسأضطر إلى الرجوع واستخدام العديد من هذه الأدوات ؛ قلبي ليس في مثل هذا التعهد. الغرض من هذه المقالة هو مشاركة بعض الأفكار معك ، وعدم التعامل مع اللكمات ، وإظهار ما يناسبني. إذا كنت تعاني من أدوات البرمجة التصريحية ، فقد تجد الراحة والبدائل. وإذا كنت تستمتع بالنموذج وأدواته ، فلا تأخذني على محمل الجد.
إذا كانت البرمجة التصريحية تعمل جيدًا بالنسبة لك ، فلن أتمكن من إخبارك بخلاف ذلك .
مزايا البرمجة التصريحية
قبل أن نستكشف حدود البرمجة التصريحية ، من الضروري فهم مزاياها.
يمكن القول إن أنجح أداة برمجة تعريفية هي قاعدة البيانات العلائقية (RDB). قد تكون حتى الأداة التصريحية الأولى. على أي حال ، تعرض RDBs الخاصيتين اللتين أعتبرهما نموذجيين للبرمجة التصريحية:
- لغة خاصة بالمجال (DSL) : الواجهة العالمية لقواعد البيانات العلائقية هي DSL تسمى لغة الاستعلام الهيكلية ، والمعروفة باسم SQL.
- يقوم DSL بإخفاء طبقة المستوى الأدنى عن المستخدم : منذ ورقة Edgar F. Codd الأصلية على RDBs ، من الواضح أن قوة هذا النموذج هي فصل الاستعلامات المرغوبة عن الحلقات الأساسية والفهارس ومسارات الوصول التي تنفذها.
قبل RDBs ، تم الوصول إلى معظم أنظمة قواعد البيانات من خلال التعليمات البرمجية الضرورية ، والتي تعتمد بشكل كبير على التفاصيل منخفضة المستوى مثل ترتيب السجلات والفهارس والمسارات المادية للبيانات نفسها. نظرًا لأن هذه العناصر تتغير بمرور الوقت ، غالبًا ما تتوقف التعليمات البرمجية عن العمل بسبب بعض التغييرات الأساسية في بنية البيانات. من الصعب كتابة الكود الناتج ، ويصعب تصحيحه ، ويصعب قراءته ويصعب الحفاظ عليه. سأخرج طرفًا وأقول أن معظم هذه الشفرة كانت ، على الأرجح ، طويلة ، مليئة بأعشاش الفئران التي يضرب بها المثل من الشرطية والتكرار والأخطاء الدقيقة التي تعتمد على الحالة.
في مواجهة ذلك ، قدمت RDBs قفزة إنتاجية هائلة لمطوري الأنظمة. الآن ، بدلاً من آلاف الأسطر من التعليمات البرمجية الضرورية ، كان لديك مخطط بيانات محدد بوضوح ، بالإضافة إلى مئات (أو حتى عشرات) من الاستفسارات. ونتيجة لذلك ، كان على التطبيقات فقط التعامل مع تمثيل تجريدي وهادف ودائم للبيانات ، وواجهتها من خلال لغة استعلام قوية وبسيطة. من المحتمل أن يكون RDB قد رفع إنتاجية المبرمجين والشركات التي وظفتهم ، من حيث الحجم.
ما هي مزايا البرمجة التصريحية المدرجة بشكل شائع؟
- قابلية القراءة / سهولة الاستخدام : عادةً ما يكون DSL أقرب إلى لغة طبيعية (مثل اللغة الإنجليزية) منه إلى الشفرة الزائفة ، وبالتالي يكون أكثر قابلية للقراءة ويسهل تعلمه من قبل غير المبرمجين.
- الإيجاز : يتم استخلاص الكثير من النماذج المعيارية بواسطة DSL ، مما يترك عددًا أقل من الخطوط للقيام بنفس العمل.
- إعادة الاستخدام : من الأسهل إنشاء كود يمكن استخدامه لأغراض مختلفة ؛ شيء معروف بصعوبة عند استخدام التركيبات الحتمية.
- Idempotence : يمكنك العمل مع الحالات النهائية والسماح للبرنامج بتحديدها لك. على سبيل المثال ، من خلال عملية upert ، يمكنك إما إدراج صف إذا لم يكن موجودًا ، أو تعديله إذا كان موجودًا بالفعل ، بدلاً من كتابة رمز للتعامل مع كلتا الحالتين.
- استعادة الأخطاء : من السهل تحديد بنية ستتوقف عند الخطأ الأول بدلاً من الاضطرار إلى إضافة مستمعين للخطأ لكل خطأ محتمل. (إذا كنت قد كتبت ثلاث عمليات نداء متداخلة في node.js ، فأنت تعرف ما أعنيه.)
- الشفافية المرجعية : على الرغم من أن هذه الميزة مرتبطة بشكل شائع بالبرمجة الوظيفية ، إلا أنها صالحة بالفعل لأي نهج يقلل من المعالجة اليدوية للحالة ويعتمد على الآثار الجانبية.
- التبادلية : إمكانية التعبير عن حالة نهائية دون الحاجة إلى تحديد الترتيب الفعلي الذي سيتم تنفيذه فيه.
في حين أن جميع المزايا المذكورة أعلاه هي مزايا البرمجة التصريحية التي يتم الاستشهاد بها بشكل شائع ، إلا أنني أود أن أختصرها في صفتين ، والتي ستكون بمثابة مبادئ توجيهية عندما أقترح نهجًا بديلًا.
- طبقة عالية المستوى مصممة لمجال معين : تُنشئ البرمجة التعريفية طبقة عالية المستوى باستخدام معلومات المجال الذي تنطبق عليه. من الواضح أننا إذا كنا نتعامل مع قواعد البيانات ، فنحن نريد مجموعة من العمليات للتعامل مع البيانات. تنبع معظم المزايا السبع المذكورة أعلاه من إنشاء طبقة عالية المستوى مصممة بدقة لمجال مشكلة معين.
- Poka-yoke (مقاومة الخداع) : تخفي طبقة عالية المستوى مصممة للمجال التفاصيل الضرورية للتنفيذ. هذا يعني أنك ترتكب أخطاء أقل بكثير لأن التفاصيل ذات المستوى المنخفض للنظام لا يمكن الوصول إليها ببساطة. هذا القيد يزيل العديد من فئات الأخطاء من التعليمات البرمجية الخاصة بك.
مشكلتان مع البرمجة التعريفية
في القسمين التاليين ، سأعرض مشكلتين رئيسيتين في البرمجة التصريحية: الانفصال وعدم الكشف . كل نقد يحتاج إلى بعبعه ، لذلك سأستخدم أنظمة قوالب HTML كمثال ملموس لأوجه القصور في البرمجة التصريحية.
مشكلة DSLs: الانفصال
تخيل أنك بحاجة إلى كتابة تطبيق ويب بعدد غير قليل من المشاهدات. لا يعد الترميز الثابت لطرق العرض هذه في مجموعة من ملفات HTML خيارًا لأن العديد من مكونات هذه الصفحات تتغير.
يبدو الحل الأكثر وضوحًا ، وهو إنشاء HTML عن طريق تسلسل السلاسل ، أمرًا مروعًا للغاية لدرجة أنك ستبحث بسرعة عن بديل. الحل القياسي هو استخدام نظام القوالب. على الرغم من وجود أنواع مختلفة من أنظمة القوالب ، فإننا سوف نتجنب اختلافاتهم لغرض هذا التحليل. يمكننا اعتبارها جميعًا متشابهة من حيث أن المهمة الرئيسية لأنظمة القوالب هي توفير بديل للكود الذي يربط سلاسل HTML باستخدام الشرطية والحلقات ، تمامًا مثل RDBs التي ظهرت كبديل للتعليمات البرمجية التي يتم تكرارها من خلال سجلات البيانات.
لنفترض أننا نتبع نظام قوالب معياري ؛ ستصادف ثلاثة مصادر للاحتكاك سأدرجها بترتيب تصاعدي من حيث الأهمية. الأول هو أن النموذج موجود بالضرورة في ملف منفصل عن التعليمات البرمجية الخاصة بك. نظرًا لأن نظام القوالب يستخدم DSL ، فإن بناء الجملة مختلف ، لذلك لا يمكن أن يكون في نفس الملف. في المشاريع البسيطة ، حيث يكون عدد الملفات منخفضًا ، قد تؤدي الحاجة إلى الاحتفاظ بملفات القوالب المنفصلة إلى مضاعفة كمية الملفات أو مضاعفتها ثلاث مرات.
قمت بفتح استثناء لقوالب Ruby المضمنة (ERB) ، لأنها مدمجة في شفرة مصدر Ruby. ليس هذا هو الحال بالنسبة للأدوات المستوحاة من ERB المكتوبة بلغات أخرى حيث يجب أيضًا تخزين هذه القوالب كملفات مختلفة.
المصدر الثاني للاحتكاك هو أن DSL لها تركيبها الخاص ، وهي صيغة مختلفة عن تلك المستخدمة في لغة البرمجة الخاصة بك. ومن ثم ، فإن تعديل DSL (ناهيك عن كتابة ما يخصك) هو أصعب بكثير. للذهاب تحت الغطاء وتغيير الأداة ، تحتاج إلى التعرف على الرموز المميزة والتحليل ، وهو أمر مثير للاهتمام وصعب ، ولكنه صعب. تصادف أن أرى هذا على أنه عيب.
قد تسأل ، "لماذا بحق الأرض تريد تعديل أداتك؟ إذا كنت تنفذ مشروعًا قياسيًا ، فيجب أن تتناسب الأداة القياسية المصاغة جيدًا مع الفاتورة ". ربما نعم ، ربما لا.
لا يتمتع DSL مطلقًا بالقوة الكاملة للغة البرمجة. إذا كان الأمر كذلك ، فلن يكون DSL بعد الآن ، بل لغة برمجة كاملة.
لكن أليس هذا هو بيت القصيد من DSL؟ لعدم توفر القوة الكاملة للغة البرمجة ، حتى نتمكن من تحقيق التجريد والقضاء على معظم مصادر الأخطاء؟ ربما نعم. ومع ذلك ، تبدأ معظم DSLs بشكل بسيط ثم تدمج تدريجياً عددًا متزايدًا من تسهيلات لغة البرمجة حتى تصبح ، في الواقع ، واحدة. أنظمة القوالب هي مثال ممتاز. دعونا نرى الميزات القياسية لأنظمة القوالب وكيف ترتبط بمرافق لغة البرمجة:
- استبدال النص داخل قالب : استبدال متغير.
- تكرار قالب : حلقات.
- تجنب طباعة قالب إذا لم يتم استيفاء أحد الشروط : الشروط الشرطية.
- الجزئيات : الروتينات الفرعية.
- المساعدون : الإجراءات الفرعية (الاختلاف الوحيد مع الأجزاء هو أن المساعدين يمكنهم الوصول إلى لغة البرمجة الأساسية ويتيحون لك الخروج من DSL Straightjacket).
هذه الحجة ، القائلة بأن DSL محدود لأنه يشتهي ويرفض في نفس الوقت قوة لغة البرمجة ، يتناسب طرديًا مع المدى الذي يمكن فيه تعيين ميزات DSL مباشرة إلى ميزات لغة البرمجة . في حالة SQL ، تكون الحجة ضعيفة لأن معظم الأشياء التي تقدمها SQL لا تشبه ما تجده في لغة البرمجة العادية. في الطرف الآخر من الطيف ، نجد أنظمة القوالب حيث تعمل كل ميزة تقريبًا على جعل DSL يتقارب نحو BASIC.
دعونا الآن نتراجع ونفكر في هذه المصادر الثلاثة للاحتكاك ، والتي لخصها مفهوم الانفصال . لأنه منفصل ، يجب أن يكون DSL موجودًا في ملف منفصل ؛ من الصعب تعديلها (بل والأكثر صعوبة في كتابة لغتك الخاصة) ، (غالبًا ، ولكن ليس دائمًا) تحتاج إلى إضافة ، واحدة تلو الأخرى ، الميزات التي تفتقدها من لغة برمجة حقيقية.
يعتبر الانفصال مشكلة متأصلة في أي DSL ، بغض النظر عن مدى جودة التصميم.
ننتقل الآن إلى مشكلة ثانية تتعلق بالأدوات التقريرية ، وهي مشكلة واسعة الانتشار ولكنها ليست متأصلة.
مشكلة أخرى: الافتقار إلى الانفتاح يؤدي إلى التعقيد
إذا كنت قد كتبت هذا المقال قبل بضعة أشهر ، لكان هذا القسم قد أطلق عليه اسم معظم الأدوات التعريفية هي # @! $ # @! معقدة لكني لا أعرف لماذا . أثناء كتابة هذا المقال ، وجدت طريقة أفضل لصياغة هذا المقال: معظم الأدوات التعريفية أكثر تعقيدًا مما يجب أن تكون عليه. سأقضي بقية هذا القسم في شرح السبب. لتحليل مدى تعقيد الأداة ، أقترح مقياسًا يسمى فجوة التعقيد . فجوة التعقيد هي الفرق بين حل مشكلة معينة بأداة مقابل حلها في المستوى الأدنى (على الأرجح رمز أمر عادي) الذي تنوي الأداة استبداله. عندما يكون الحل الأول أكثر تعقيدًا من الأخير ، فإننا نواجه فجوة التعقيد. وبكلمات أكثر تعقيدًا ، أعني المزيد من سطور التعليمات البرمجية ، والتعليمات البرمجية التي يصعب قراءتها ، وأصعب في التعديل ، ويصعب الحفاظ عليها ، ولكن ليس بالضرورة كل هذه الأسطر في نفس الوقت.
يرجى ملاحظة أننا لا نقارن حل المستوى الأدنى مع أفضل أداة ممكنة ، ولكن بالأحرى لا نقارن أي أداة. وهذا يعكس المبدأ الطبي المتمثل في "أولاً ، لا ضرر ولا ضرار" .
علامات الأداة التي بها فجوة تعقيد كبيرة هي:
- الشيء الذي يستغرق بضع دقائق لوصفه بتفاصيل غنية بعبارات حتمية سوف يستغرق ساعات في البرمجة باستخدام الأداة ، حتى عندما تعرف كيفية استخدام الأداة.
- تشعر أنك تعمل باستمرار حول الأداة بدلاً من الأداة.
- أنت تكافح من أجل حل مشكلة مباشرة تنتمي بشكل مباشر إلى مجال الأداة التي تستخدمها ، ولكن أفضل إجابة Stack Overflow التي تجدها تصف حلاً بديلًا .
- عندما يمكن حل هذه المشكلة الواضحة جدًا عن طريق ميزة معينة (غير موجودة في الأداة) وسترى مشكلة Github في المكتبة التي تتميز بمناقشة طويلة للميزة المذكورة مع تخللها +1 .
- مزمن ، حكة ، اشتياق للتخلي عن الأداة والقيام بكل شيء بنفسك داخل حلقة for-.
ربما وقعت فريسة للعاطفة هنا لأن أنظمة القوالب ليست معقدة إلى هذا الحد ، لكن فجوة التعقيد الصغيرة نسبيًا هذه ليست ميزة لتصميمها ، بل لأن مجال التطبيق بسيط جدًا (تذكر ، نحن فقط ننشئ HTML هنا ). كلما تم استخدام نفس النهج لمجال أكثر تعقيدًا (مثل إدارة التكوين) ، فقد تحول فجوة التعقيد مشروعك بسرعة إلى مستنقع.
ومع ذلك ، فليس من غير المقبول بالضرورة أن تكون الأداة أكثر تعقيدًا إلى حد ما من المستوى الأدنى الذي تنوي استبداله ؛ إذا كانت الأداة تنتج رمزًا أكثر قابلية للقراءة وموجزًا وصحيحًا ، فقد يكون الأمر يستحق ذلك. إنها مشكلة عندما تكون الأداة أكثر تعقيدًا بعدة مرات من المشكلة التي تحل محلها ؛ هذا غير مقبول بشكل قاطع. صرح بريان كيرنيغان بشكل مشهور بأن " التحكم في التعقيد هو جوهر برمجة الكمبيوتر. إذا كانت هناك أداة تضيف تعقيدًا كبيرًا إلى مشروعك ، فلماذا حتى تستخدمها؟
السؤال هو ، لماذا بعض الأدوات التعريفية أكثر تعقيدًا مما يجب أن تكون؟ أعتقد أنه سيكون من الخطأ إلقاء اللوم على سوء التصميم. مثل هذا التفسير العام ، هجوم شامل على مؤلفي هذه الأدوات ، ليس عادلاً. يجب أن يكون هناك تفسير أكثر دقة واستنارة.
رأيي هو أن أي أداة تقدم واجهة عالية المستوى لاستخلاص مستوى أدنى يجب أن تتكشف عن هذا المستوى الأعلى من المستوى الأدنى. يأتي مفهوم الانكشاف من أعظم إبداع كريستوفر ألكساندر ، طبيعة النظام - ولا سيما المجلد الثاني. إنه (بشكل يائس) خارج نطاق هذه المقالة (ناهيك عن فهمي) لتلخيص الآثار المترتبة على هذا العمل الضخم لتصميم البرمجيات ؛ أعتقد أن تأثيره سيكون هائلاً في السنوات القادمة. كما أنه يتجاوز هذه المقالة لتوفير تعريف صارم للعمليات التي تتكشف. سأستخدم هنا المفهوم بطريقة إرشادية.
عملية الكشف هي عملية تخلق ، بطريقة تدريجية ، هيكلًا إضافيًا دون إبطال الهيكل الحالي. في كل خطوة ، يظل كل تغيير (أو تمايز ، لاستخدام مصطلح الإسكندر) منسجمًا مع أي بنية سابقة ، عندما تكون البنية السابقة ، ببساطة ، سلسلة متبلورة من التغييرات السابقة.
ومن المثير للاهتمام أن نظام Unix هو مثال رائع لتكشف عن مستوى أعلى من مستوى أدنى. في نظام التشغيل Unix ، هناك ميزتان معقدتان لنظام التشغيل ، وهما الوظائف المجمعة و coroutines (الأنابيب) ، وهما مجرد امتدادات للأوامر الأساسية. بسبب بعض قرارات التصميم الأساسية ، مثل جعل كل شيء تيارًا من البايتات ، والصدفة هي برنامج userland وملفات I / O قياسية ، فإن Unix قادر على توفير هذه الميزات المعقدة بأقل قدر من التعقيد.
للتأكيد على سبب كون هذه أمثلة ممتازة على الكشف ، أود أن أقتبس بعض المقتطفات من بحث عام 1979 بقلم دينيس ريتشي ، أحد مؤلفي يونكس:
في الوظائف المجمعة :
... جعل مخطط التحكم في العملية الجديد على الفور بعض الميزات القيمة للغاية تافهة في التنفيذ ؛ على سبيل المثال العمليات المنفصلة (مع
&
) والاستخدام المتكرر للقذيفة كأمر. يتعين على معظم الأنظمة توفير نوع من تسهيلاتbatch job submission
الخاصة ومترجم أوامر خاص للملفات المتميزة عن تلك المستخدمة بشكل تفاعلي.
على الكوروتينات :
إن عبقرية خط أنابيب يونكس بالتحديد هي أنه مبني من نفس الأوامر المستخدمة باستمرار بطريقة بسيطة.
إن هذه الأناقة والبساطة ، كما أجادل ، تأتي من عملية تتكشف . تم الكشف عن وظائف الدُفعات و coroutines من الهياكل السابقة (يتم تشغيل الأوامر في غلاف userland). أعتقد أنه نظرًا لفلسفة الحد الأدنى والموارد المحدودة للفريق الذي أنشأ Unix ، فقد تطور النظام تدريجيًا ، وعلى هذا النحو ، كان قادرًا على دمج الميزات المتقدمة دون الرجوع إلى الميزات الأساسية لأنه لم تكن هناك موارد كافية تفعل خلاف ذلك.
في حالة عدم وجود عملية تتكشف ، سيكون المستوى العالي أكثر تعقيدًا من اللازم. بعبارة أخرى ، ينبع تعقيد معظم الأدوات التصريحية من حقيقة أن مستواها العالي لا يتكشف عن المستوى المنخفض الذي ينوون استبداله.
هذا النقص في الانفتاح ، إذا كنت تغفر للمصطلحات الجديدة ، يتم تبريره بشكل روتيني بضرورة حماية المستخدم من المستوى الأدنى. يأتي هذا التركيز على poka-yoke (حماية المستخدم من أخطاء المستوى المنخفض) على حساب فجوة تعقيد كبيرة تدمر نفسها لأن التعقيد الإضافي سيولد فئات جديدة من الأخطاء. لإضافة الطين بلة ، هذه الفئات من الأخطاء ليس لها علاقة بمجال المشكلة بل بالأداة نفسها. لن نذهب بعيدًا إذا وصفنا هذه الأخطاء بأنها علاجي المنشأ.
تعتبر أدوات القوالب التصريحية ، على الأقل عند تطبيقها على مهمة إنشاء عروض HTML ، حالة نموذجية لمستوى عالٍ تدير ظهرها للمستوى المنخفض الذي تنوي استبداله. كيف ذلك؟ نظرًا لأن إنشاء أي عرض غير تافه يتطلب المنطق وأنظمة القوالب ، خاصة تلك التي لا تحتوي على منطق ، فإن المنطق يزيل المنطق من خلال الباب الرئيسي ثم يهرب بعضًا منه مرة أخرى عبر باب القط.
ملحوظة: التبرير الأضعف لفجوة التعقيد الكبيرة هو عندما يتم تسويق أداة ما على أنها سحرية ، أو شيء يعمل فقط ، من المفترض أن يكون غموض المستوى المنخفض أحد الأصول لأنه من المفترض دائمًا أن تعمل الأداة السحرية دون أن تفهمها. لماذا او كيف. من واقع خبرتي ، كلما كانت الأداة أكثر سحرية ، زادت سرعة تحويل حماسي إلى إحباط.
لكن ماذا عن فصل الهموم؟ ألا يجب أن يبقى المنطق والعرض منفصلين؟ الخطأ الأساسي ، هنا ، هو وضع منطق الأعمال ومنطق العرض في نفس الحقيبة. من المؤكد أن منطق الأعمال ليس له مكان في القالب ، لكن منطق العرض موجود بالرغم من ذلك. يؤدي استبعاد المنطق من القوالب إلى دفع منطق العرض إلى الخادم حيث يتم استيعابها بشكل محرج. أنا مدين بالصياغة الواضحة لهذه النقطة لأليكسي بورونين ، الذي قدم حجة ممتازة لها في هذه المقالة.
شعوري هو أن ما يقرب من ثلثي عمل النموذج يكمن في منطق العرض الخاص به ، بينما يتعامل الثلث الآخر مع القضايا العامة مثل ربط السلاسل ، وإغلاق العلامات ، والهروب من الأحرف الخاصة ، وما إلى ذلك. هذه هي طبيعة المستوى المنخفض ذات الوجهين لتوليد طرق عرض HTML. تتعامل أنظمة القوالب بشكل مناسب مع النصف الثاني ، لكنها لا تعمل بشكل جيد مع النصف الأول. تُدير القوالب التي لا تعتمد على المنطق ظهورها لهذه المشكلة ، مما يجبرك على حلها بشكل محرج. تعاني أنظمة القوالب الأخرى لأنها تحتاج حقًا إلى توفير لغة برمجة غير تافهة حتى يتمكن مستخدموها بالفعل من كتابة منطق العرض التقديمي.
لنلخص؛ تعاني أدوات القوالب التصريحية من الأسباب التالية:
- إذا كان عليهم أن يتكشفوا من مجال مشكلتهم ، فسيتعين عليهم توفير طرق لتوليد أنماط منطقية ؛
- إن DSL الذي يوفر المنطق ليس في الواقع DSL ، ولكنه لغة برمجة. لاحظ أن المجالات الأخرى ، مثل إدارة التكوين ، تعاني أيضًا من عدم وجود "كشف".
أود أن أنهي النقد بحجة منفصلة منطقيًا عن خيط هذه المقالة ، لكن لها صدى عميق في جوهرها العاطفي: لدينا وقت محدود للتعلم. الحياة قصيرة ، وفوق كل ذلك ، نحن بحاجة إلى العمل. في مواجهة قيودنا ، نحتاج إلى قضاء وقتنا في تعلم أشياء ستكون مفيدة وتتحمل الوقت ، حتى في مواجهة التكنولوجيا سريعة التغير. هذا هو السبب في أنني أنصحك باستخدام الأدوات التي لا تقدم حلاً فحسب ، بل تلقي في الواقع ضوءًا ساطعًا على مجال قابليتها للتطبيق. تعلمك RDBs عن البيانات ، ويعلمك Unix عن مفاهيم نظام التشغيل ، ولكن مع الأدوات غير المرضية التي لا تتكشف ، شعرت دائمًا أنني كنت أتعلم تعقيدات حل دون المستوى بينما بقيت في حالة جهل بشأن طبيعة المشكلة تعتزم حلها.
الاستدلال الذي أقترحه عليك هو ، أدوات القيمة التي تضيء مجال مشكلتهم ، بدلاً من الأدوات التي تحجب مجال مشكلتهم خلف الميزات المزعومة .
نهج التوأم
للتغلب على مشكلتي البرمجة التصريحية ، التي قدمتها هنا ، أقترح نهجًا مزدوجًا:
- استخدم لغة محددة لمجال بنية البيانات (dsDSL) للتغلب على الانفصال.
- قم بإنشاء مستوى عالٍ يتكشف من المستوى الأدنى ، للتغلب على فجوة التعقيد.
DSL
بنية البيانات DSL (dsDSL) هي DSL تم إنشاؤها باستخدام هياكل البيانات الخاصة بلغة البرمجة . الفكرة الأساسية هي استخدام هياكل البيانات الأساسية المتوفرة لديك ، مثل السلاسل والأرقام والمصفوفات والكائنات والوظائف ، ودمجها لإنشاء تجريدات للتعامل مع مجال معين.
نريد الاحتفاظ بقوة التصريح عن الهياكل أو الإجراءات (مستوى عالٍ) دون الحاجة إلى تحديد الأنماط التي تنفذ هذه التركيبات (مستوى منخفض). نريد التغلب على الفصل بين DSL ولغة البرمجة لدينا حتى نتمتع بحرية استخدام القوة الكاملة للغة البرمجة متى احتجنا إليها. هذا ليس ممكنًا فقط ولكنه مباشر من خلال dsDSLs.
إذا سألتني قبل عام ، كنت سأعتقد أن مفهوم dsDSL كان جديدًا ، ثم يومًا ما ، أدركت أن JSON نفسها كانت مثالًا رائعًا على هذا النهج! يتكون كائن JSON الذي تم تحليله من هياكل البيانات التي تمثل إدخالات البيانات بشكل تصريحي من أجل الحصول على مزايا DSL مع تسهيل التحليل والتعامل من داخل لغة برمجة. (قد يكون هناك dsDSLs أخرى ، لكن حتى الآن لم أجد أيًا منها. إذا كنت تعرف واحدة ، سأكون ممتنًا لذكرها في قسم التعليقات.)
مثل JSON ، يحتوي dsDSL على السمات التالية:
- يتكون من مجموعة صغيرة جدًا من الوظائف: JSON لها وظيفتان رئيسيتان ،
parse
stringify
. - تستقبل وظائفها بشكل شائع وسيطات معقدة ومتكررة: JSON التي تم تحليلها هي مصفوفة أو كائن يحتوي عادة على مصفوفات وكائنات أخرى بداخله.
- تتوافق مدخلات هذه الوظائف مع نماذج محددة جدًا: يحتوي JSON على مخطط تحقق صريح ومُطبَّق بصرامة لمعرفة الصالح من البنى غير الصالحة.
- يمكن احتواء كل من مدخلات ومخرجات هذه الوظائف وتوليدها بواسطة لغة برمجة بدون بناء جملة منفصل.
لكن dsDSLs تتجاوز JSON بعدة طرق. لنقم بإنشاء dsDSL لتوليد HTML باستخدام Javascript. لاحقًا سأتطرق إلى مسألة ما إذا كان هذا النهج يمكن أن يمتد إلى لغات أخرى (المفسد: يمكن بالتأكيد القيام به في Ruby و Python ، ولكن ربما ليس في C).

HTML هي لغة ترميز تتكون من tags
محددة بأقواس زاوية ( <
و >
). قد يكون لهذه العلامات سمات ومحتويات اختيارية. السمات هي ببساطة قائمة بسمات المفتاح / القيمة ، وقد تكون المحتويات إما نصًا أو علامات أخرى. تعد كل من السمات والمحتويات اختيارية لأي علامة معينة. أنا أبسط بعض الشيء ، لكنها دقيقة.
تتمثل الطريقة المباشرة لتمثيل علامة HTML في dsDSL في استخدام مصفوفة تحتوي على ثلاثة عناصر: - العلامة: سلسلة. - السمات: كائن (عادي ، مفتاح / نوع قيمة) أو undefined
(إذا لم تكن هناك سمات ضرورية). - المحتويات: سلسلة (نص) أو مصفوفة (علامة أخرى) أو undefined
(إذا لم تكن هناك محتويات).
على سبيل المثال ، يمكن كتابة <a href="views">Index</a>
كـ ['a', {href: 'views'}, 'Index']
.
إذا أردنا تضمين عنصر الارتساء هذا في div
مع links
الفئة ، فيمكننا كتابة: ['div', {class: 'links'}, ['a', {href: 'views'}, 'Index']]
.
لسرد العديد من علامات html في نفس المستوى ، يمكننا تغليفها في مصفوفة:
[ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]
يمكن تطبيق نفس المبدأ على إنشاء علامات متعددة داخل علامة:
['body', [ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]]
بالطبع ، dsDSL هذا لن يجعلنا بعيدًا إذا لم ننشئ HTML منه. نحتاج إلى دالة generate
تأخذ dsDSL وتنتج سلسلة مع HTML. لذلك إذا قمنا بتشغيل generate (['a', {href: 'views'}, 'Index'])
، فسنحصل على السلسلة <a href="views">Index</a>
.
تكمن الفكرة وراء أي DSL في تحديد عدد قليل من التركيبات بهيكل محدد يتم تمريره بعد ذلك إلى دالة. في هذه الحالة ، الهيكل الذي يتكون من dsDSL هو هذه المصفوفة ، التي تتكون من عنصر إلى ثلاثة عناصر ؛ هذه المصفوفات لها بنية محددة. إذا generate
الإنشاء تمامًا من صحة المدخلات الخاصة به (ومن السهل والمهم التحقق من صحة الإدخال تمامًا ، نظرًا لأن قواعد التحقق من الصحة هذه هي التناظرية الدقيقة لبناء جملة DSL) ، فسوف يخبرك بالضبط أين أخطأت في الإدخال. بعد فترة ، ستبدأ في التعرف على ما يميز البنية الصالحة في dsDSL ، وستكون هذه البنية موحية بشكل كبير للشيء الأساسي الذي تولده.
الآن ، ما هي مزايا dsDSL الذي يتعارض مع DSL؟
- يعد dsDSL جزءًا لا يتجزأ من التعليمات البرمجية الخاصة بك. يؤدي إلى انخفاض عدد الخطوط وعدد الملفات وتقليل إجمالي في النفقات العامة.
- من السهل تحليل dsDSLs (وبالتالي أسهل في التنفيذ والتعديل). التحليل هو مجرد تكرار من خلال عناصر مصفوفة أو كائن. وبالمثل ، فإن dsDSLs سهلة التصميم نسبيًا لأنه بدلاً من إنشاء بناء جملة جديد (سيكره الجميع) يمكنك التمسك ببنية لغة البرمجة الخاصة بك (التي يكرهها الجميع ولكنهم يعرفون ذلك بالفعل على الأقل).
- يتمتع dsDSL بكل قوة لغة البرمجة. هذا يعني أن dsDSL ، عند استخدامه بشكل صحيح ، يتمتع بميزة كل من أداة المستوى العالي والمنخفض.
الآن ، الادعاء الأخير قوي ، لذا سأقضي بقية هذا القسم في دعمه. ماذا أعني بوظيفة بشكل صحيح ؟ لرؤية هذا عمليًا ، دعنا نفكر في مثال نريد فيه إنشاء جدول لعرض المعلومات من مصفوفة تسمى DATA
.
var DATA = [ {id: 1, description: 'Product 1', price: 20, onSale: true, categories: ['a']}, {id: 2, description: 'Product 2', price: 60, onSale: false, categories: ['b']}, {id: 3, description: 'Product 3', price: 120, onSale: false, categories: ['a', 'c']}, {id: 4, description: 'Product 4', price: 45, onSale: true, categories: ['a', 'b']} ]
في التطبيق الحقيقي ، سيتم إنشاء DATA
ديناميكيًا من استعلام قاعدة البيانات.
علاوة على ذلك ، لدينا متغير FILTER
والذي ، عند تهيئته ، سيكون مصفوفة بالفئات التي نريد عرضها.
نريد طاولتنا إلى:
- عرض رؤوس الجدول.
- لكل منتج ، اعرض الحقول: الوصف والسعر والفئات.
- لا تطبع حقل
id
، بل أضفهid
لكل صف. الإصدار البديل: أضف سمةid
لكل عنصرtr
. - ضع فئة
onSale
إذا كان المنتج معروضًا للبيع. - فرز المنتجات حسب السعر التنازلي.
- تصفية منتجات معينة حسب الفئة. إذا كان
FILTER
عبارة عن مصفوفة فارغة ، فسنعرض جميع المنتجات. بخلاف ذلك ، سنقوم فقط بعرض المنتجات التي تحتوي على فئة المنتج ضمنFILTER
.
يمكننا إنشاء منطق العرض التقديمي الذي يطابق هذا المطلب في حوالي 20 سطرًا من التعليمات البرمجية:
function drawTable (DATA, FILTER) { var printableFields = ['description', 'price', 'categories']; DATA.sort (function (a, b) {return a.price - b.price}); return ['table', [ ['tr', dale.do (printableFields, function (field) { return ['th', field]; })], dale.do (DATA, function (product) { var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; }); return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]]; })]; }) ]]; }
أقر بأن هذا ليس مثالًا مباشرًا ، ومع ذلك ، فهو يمثل رؤية بسيطة إلى حد ما للوظائف الأساسية الأربعة للتخزين المستمر ، والمعروف أيضًا باسم CRUD. أي تطبيق ويب غير تافه سيكون له طرق عرض أكثر تعقيدًا من ذلك.
دعنا الآن نرى ما يفعله هذا الرمز. أولاً ، تحدد دالة ، drawTable
، لتحتوي على منطق العرض الخاص برسم جدول المنتج. تستقبل هذه الوظيفة DATA
و FILTER
كمعلمات ، لذا يمكن استخدامها لمجموعات بيانات ومرشحات مختلفة. drawTable
يفي بالدور المزدوج الجزئي والمساعد.
var drawTable = function (DATA, FILTER) {
المتغير الداخلي ، printableFields
، هو المكان الوحيد الذي تحتاج فيه إلى تحديد الحقول القابلة للطباعة ، وتجنب التكرار وعدم الاتساق في مواجهة المتطلبات المتغيرة.
var printableFields = ['description', 'price', 'categories'];
ثم نقوم بفرز DATA
وفقًا لسعر منتجاتها. لاحظ أن معايير الفرز المختلفة والأكثر تعقيدًا سيكون من السهل تنفيذها نظرًا لأن لدينا لغة البرمجة بأكملها تحت تصرفنا.
DATA.sort (function (a, b) {return a.price - b.price});
هنا نعيد كائنًا حرفيًا ؛ مصفوفة تحتوي على table
كعنصره الأول ومحتوياته كعنصر ثان. هذا هو تمثيل dsDSL لـ <table>
الذي نريد إنشاءه.
return ['table', [
نقوم الآن بإنشاء صف برؤوس الجدول. لإنشاء محتوياته ، نستخدم dale.do وهي دالة مثل Array.map ، ولكنها تعمل أيضًا مع الكائنات. سنقوم بتكرار الحقول printableFields
وإنشاء رؤوس جدول لكل منها:
['tr', dale.do (printableFields, function (field) { return ['th', field]; })],
لاحظ أننا قمنا للتو بتنفيذ التكرار ، وهو العمود الفقري لتوليد HTML ، ولم نكن بحاجة إلى أي تركيبات DSL ؛ كنا بحاجة فقط إلى وظيفة لتكرار بنية البيانات وإرجاع dsDSLs. كان من الممكن لوظيفة أصلية مماثلة ، أو وظيفة ينفذها المستخدم ، أن تفعل الحيلة أيضًا.
كرر الآن من خلال المنتجات الواردة في DATA
.
dale.do (DATA, function (product) {
نتحقق مما إذا كان هذا المنتج قد تركه FILTER
. إذا كان FILTER
فارغًا ، فسنقوم بطباعة المنتج. إذا لم يكن FILTER
فارغًا ، فسنقوم بالتكرار خلال فئات المنتج حتى نجد واحدة مضمنة في FILTER
. نقوم بذلك باستخدام dale.stop.
var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; });
لاحظ تعقيد الشرط ؛ إنه مصمم خصيصًا لمتطلباتنا ولدينا الحرية الكاملة للتعبير عنها لأننا في لغة برمجة بدلاً من DSL.
إذا كانت matches
false
، نعيد مصفوفة فارغة (لذلك لا نطبع هذا المنتج). بخلاف ذلك ، نعيد <tr>
الصحيح وفئته ونقوم بالتكرار من خلال printableFields
، حسنًا ، نطبع الحقول.
return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]];
بالطبع نغلق كل ما فتحناه. أليست اللغة ممتعة؟
})]; }) ]]; }
الآن ، كيف ندمج هذا الجدول في سياق أوسع؟ نكتب دالة باسم drawAll
تستدعي جميع الوظائف التي تولد المشاهدات. بصرف النظر عن drawTable
، قد يكون لدينا أيضًا drawHeader
و drawFooter
أخرى قابلة للمقارنة ، وكلها ستعيد dsDSLs .
var drawAll = function () { return generate ([ drawHeader (), drawTable (DATA, FILTER), drawFooter () ]); }
إذا لم يعجبك شكل الكود أعلاه ، فلن يقنعك أي شيء أقوله. هذا هو dsDSL في أفضل حالاته . يمكنك أيضًا التوقف عن قراءة المقالة (وإسقاط تعليق متوسط أيضًا لأنك حصلت على الحق في القيام بذلك إذا كنت قد وصلت إلى هذا الحد!). But seriously, if the code above doesn't strike you as elegant, nothing else in this article will.
For those who are still with me, I would like to go back to the main claim of this section, which is that a dsDSL has the advantages of both the high and the low level :
- The advantage of the low level resides in writing code whenever we want, getting out of the straightjacket of the DSL.
- The advantage of the high level resides in using literals that represent what we want to declare and letting the functions of the tool convert that into the desired end state (in this case, a string with HTML).
But how is this truly different from purely imperative code? I think ultimately the elegance of the dsDSL approach boils down to the fact that code written in this way mostly consists of expressions, instead of statements. More precisely, code that uses a dsDSL is almost entirely composed of:
- Literals that map to lower level structures.
- Function invocations or lambdas within those literal structures that return structures of the same kind.
Code that consists mostly of expressions and which encapsulate most statements within functions is extremely succinct because all patterns of repetition can be easily abstracted. You can write arbitrary code as long as that code returns a literal that conforms to a very specific, non-arbitrary form.
A further characteristic of dsDSLs (which we don't have time to explore here) is the possibility of using types to increase the richness and succinctness of the literal structures. I will expound on this issue on a future article.
Might it be possible to create dsDSLs beyond Javascript, the One True Language? I think that it is, indeed, possible, as long as the language supports:
- Literals for: arrays, objects (associative arrays), function invocations, and lambdas.
- Runtime type detection
- Polymorphism and dynamic return types
I think this means that dsDSLs are tenable in any modern dynamic language (ie: Ruby, Python, Perl, PHP), but probably not in C or Java.
Walk, Then Slide: How To Unfold The High From The Low
In this section I will attempt to show a way for unfolding a high level tool from its domain. In a nutshell, the approach consists of the following steps
- Take two to four problems that are representative instances of a problem domain. These problems should be real. Unfolding the high level from the low one is a problem of induction, so you need real data to come up with representative solutions.
- Solve the problems with no tool in the most straightforward way possible.
- Stand back, take a good look at your solutions, and notice the common patterns among them.
- Find the patterns of representation (high level).
- Find the patterns of generation (low level).
- Solve the same problems with your high level layer and verify that the solutions are indeed correct.
- If you feel that you can easily represent all the problems with your patterns of representation, and the generation patterns for each of these instances produce correct implementations, you're done. Otherwise, go back to the drawing board.
- If new problems appear, solve them with the tool and modify it accordingly.
- The tool should converge asymptotically to a finished state, no matter how many problems it solves. In other words, the complexity of the tool should remain constant, rather than growing with the amount of problems it solves.
Now, what the hell are patterns of representation and patterns of generation ? I'm glad you asked. The patterns of representation are the patterns in which you should be able to express a problem that belongs to the domain that concerns your tool. It is an alphabet of structures that allows you to write any pattern you might wish to express within its domain of applicability. In a DSL, these would be the production rules. Let's go back to our dsDSL for generating HTML.
The patterns of representation for HTML are the following:
- A single tag:
['TAG']
- A single tag with attributes:
['TAG', {attribute1: value1, attribute2: value2, ...}]
- A single tag with contents:
['TAG', 'CONTENTS']
- A single tag with both attributes and contents:
['TAG', {attribute1: value1, ...}, 'CONTENTS']
- A single tag with another tag inside:
['TAG1', ['TAG2', ...]]
- A group of tags (standalone or inside another tag):
[['TAG1', ...], ['TAG2', ...]]
- Depending on a condition, place a tag or no tag:
condition ? ['TAG', ...] : []
/ Depending on a condition, place an attribute or no attribute:['TAG', {class: condition ? 'someClass': undefined}, ...]
These instances can be represented with the dsDSL notation we determined in the previous section. And this is all you need to represent any HTML you might need. More sophisticated patterns, such as conditional iteration through an object to generate a table, may be implemented with functions that return the patterns of representation above, and these patterns map directly to HTML tags.
If the patterns of representation are the structures you use to express what you want, the patterns of generation are the structures your tool will use to convert patterns of representation into the lower level structures. For HTML, these are the following:
- Validate the input (this is actually is an universal pattern of generation).
- Open and close tags (but not the void tags, like
<input>
, which are self-closing). - Place attributes and contents, escaping special characters (but not the contents of the
<style>
and<script>
tags).
Believe it or not, these are the patterns you need to create an unfolding dsDSL layer that generates HTML. Similar patterns can be found for generating CSS. In fact, lith does both, in ~250 lines of code.
One last question remains to be answered: What do I mean by walk, then slide ? When we deal with a problem domain, we want to use a tool that delivers us from the nasty details of that domain. In other words, we want to sweep the low level under the rug, the faster the better. The walk, then slide approach proposes exactly the opposite: spend some time on the low level. Embrace its quirks, and understand which are essential and which can be avoided in the face of a set of real, varied, and useful problems.
After walking in the low level for some time and solving useful problems, you will have a sufficiently deep understanding of their domain. The patterns of representation and generation will then arise naturally; they are wholly derived from the nature of the problem they intend to solve. You can then write code that employs them. If they work, you will be able to slide through problems where you recently had to walk through them. Sliding means many things; it implies speed, precision and lack of friction. Maybe more importantly, this quality can be felt; when solving problems with this tool, do you feel like you're walking through the problem, or do you feel that you're sliding through it?
Maybe the most important thing about an unfolded tool is not the fact that it frees us from having to deal with the low level. Rather, by capturing the empiric patterns of repetition in the low level, a good high level tool allows us to understand fully the domain of applicability.
An unfolded tool will not just solve a problem - it will enlighten you about the problem's structure.
So, don't run away from a worthy problem. First walk around it, then slide through it.