نماذج عامة و metaprogramming: Go و Rust و Swift و D وغيرها


في بعض مجالات البرمجة ، من الطبيعي أن ترغب في كتابة بنية بيانات أو خوارزمية يمكنها العمل مع عناصر من أنواع مختلفة. على سبيل المثال ، قائمة بالأدوية أو خوارزمية الفرز التي تحتاج فقط إلى وظيفة مقارنة. في العديد من اللغات ، يتم تقديم طرق مختلفة لحل هذه المشكلة: من مجرد الإشارة إلى الوظائف المشتركة المناسبة (C ، Go) إلى المبرمجين إلى الأنظمة العامة القوية التي أصبحت Turing كاملة ( Rust ، C ++ ). في هذه المقالة سأتحدث عن أنظمة عامة من لغات مختلفة وتنفيذها. سأبدأ بحل المشكلة في اللغات دون وجود نظام مماثل (مثل C) ، ثم سأوضح كيف تؤدي الإضافة التدريجية للإضافات إلى أنظمة من لغات أخرى.

أجد أن الأدوية العامة هي خيار مثير للاهتمام ، لأنها حالة خاصة بسيطة لمشكلة البرمجة العامة العامة: كتابة البرامج التي يمكنها إنشاء فئات من البرامج الأخرى. كدليل على ذلك ، سأوضح كيف يمكن اعتبار ثلاث طرق مختلفة وعامة تمامًا لبرمجة metaprograming امتدادات متعددة الاتجاهات في فضاء الأنظمة العامة: اللغات الديناميكية مثل Python ، وأنظمة الماكرو الإجرائية مثل Template Haskel ، والتجميع المرحلي مثل Zig و Terra .

نظرة عامة


قمت برسم مخطط كتلة لجميع الأنظمة الموضحة في المقالة حتى تتمكن من تقديم محتوياتها وكيفية ترابط هذه الأنظمة:



الأفكار الرئيسية


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

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

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

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

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

يمكن توسيع كل من المخططات الموضحة للعمل مع الأدوية الجنيسة في اتجاهات مختلفة ، إذا كنت بحاجة إلى المزيد من الميزات أو الأمان ، وقد توصل مؤلفو اللغات المختلفة إلى حلول مثيرة جدًا للاهتمام. على سبيل المثال ، يمكن استخدام كلا النهجين في Rust و C #!

حزم


لنبدأ بمثال على العبوة الأساسية في Go:

 type Stack struct { values []interface{} } func (this *Stack) Push(value interface{}) { this.values = append(this.values, value) } func (this *Stack) Pop() interface{} { x := this.values[len(this.values)-1] this.values = this.values[:len(this.values)-1] return x } 

أيضًا ، يتم استخدام التعبئة في C ( void* ) و Go ( interface{} ) و Java ( Object ) ما قبل عام و Object -C ( id ) ما قبل العام.

معبأة الجنيسة مع يهرس أنواع


طريقة التعبئة والتغليف الرئيسية لها عيوب:

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

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

بدأت كل من Java و Objective-C بالتعبئة المعتادة ، ثم اكتسبت لاحقًا أدوات لغة للأدوية ذات النوع المائل ، من أجل التوافق ، باستخدام نفس أنواع المجموعات كما كان من قبل ، ولكن مع المعلمات الاختيارية للأنواع العامة. فكر في مثال من مقالة Wikipedia حول الأدوية العامة في Java :

 List v = new ArrayList(); v.add("test"); // A String that cannot be cast to an Integer Integer i = (Integer)v.get(0); // Run time error List<String> v = new ArrayList<String>(); v.add("test"); Integer i = v.get(0); // (type error) compilation-time error 

الأدوية المشتقة المستخلصة ذات الأداء الموحد


يطور OCaml فكرة النظرة الموحدة. لا توجد أنواع بدائية تحتاج إلى موضع تغليف إضافي (حيث يجب أن تتحول int إلى Integer أجل الدخول إلى قائمة ArrayList في Java) ، لأن كل شيء معبأ بالفعل أو يمثل بواسطة قيمة عددية بحجم المؤشر ، أي أن كل شيء يناسب كلمة آلة واحدة. لكن عندما ينظر جامع البيانات المهملة إلى البيانات المخزنة في هياكل عامة ، فإنه يحتاج إلى التمييز بين المؤشرات والأرقام ، بحيث يتم تمييز الأرقام بحرف واحد ، بحيث لا تحتوي المؤشرات المحاذاة بشكل صحيح على بت واحد ، مما يترك نطاقات 31 أو 63 بت فقط.

يحتوي OCaml أيضًا على نظام لاستنتاج الكتابة ، بحيث يمكنك كتابة دالة وسيقوم المترجم بإخراج النوع العام الأنسب إذا لم تشرِّفه ، وهكذا تبدو الوظائف وكأنها لغة مكتوبة ديناميكيًا:

 let first (head :: tail) = head (* inferred type: 'a list -> 'a *) 

يمكن أن يسمى النوع المعطى "دالة من قائمة عناصر النوع 'a إلى شيء مع النوع 'a ". هذا يعني أن نوع الإرجاع سيكون هو نفسه نوع القائمة ، ويمكن أن يكون أي نوع.

واجهات


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

الجداول طريقة واجهة


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

هذه هي الطريقة التي يتم بها تطبيق أنواع interface في كائنات Go و dyn trait في Rust. عندما تقوم بإلقاء نوع على نوع واجهة ما تقوم بتنفيذه ، يتم إنشاء مجمّع للواجهة التي تحتوي على مؤشر للكائن المصدر ومؤشر إلى vtable للوظائف الخاصة بالنوع. ولكن هذا يتطلب مستوى إضافيًا من المعالجة غير المباشرة للمؤشرات ومخططًا آخر. لذلك ، فإن الفرز في Go يستخدم واجهة الحاوية باستخدام طريقة Swap ، ولا يأخذ شريحة الواجهة القابلة للمقارنة ، لأن هذا سيتطلب وضع شريحة جديدة تمامًا من أنواع الواجهات التي سيتم فرزها في الذاكرة بدلاً من الشريحة الأصلية!

وجوه المنحى البرمجة


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

بالإضافة إلى توفير ميزات إضافية ، فإن تضمين vtable في كل كائن يحل مشكلة الحاجة إلى إنشاء أنواع جديدة من الواجهة باستخدام عنونة غير مباشرة. بخلاف Go ، في Java ، يمكن أن تقوم دالة sort بتطبيق الواجهة Comparable على الأنواع التي تنفذها.

انعكاس


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

اللغات التي تستخدم الانعكاس للتسلسل والوظائف الأخرى: Java و C # و Go.

اللغات المكتوبة بشكل حيوي


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

يمكنك القول: "لكن اللغات الديناميكية لا تعمل بهذه الطريقة ، فهي تطبق كل شيء باستخدام جداول التجزئة!" تعد جداول التجزئة مجرد بنية بيانات جيدة لإنشاء الجداول القابلة للتحرير بمعلومات الكتابة. بالإضافة إلى ذلك ، يعمل بعض المترجمين الفوريين ، مثل CPython ، بهذه الطريقة. في JIT عالي الأداء ، يقول V8 ، هناك الكثير من جداول الأنواع الافتراضية ومعلومات الانعكاس. في V8 ، تشبه الفئات المخفية (vtables ومعلومات الانعكاس) وهيكل الكائنات ما يمكنك رؤيته في Java VM ، مع إمكانية استبدال الكائنات بجداول أنواع افتراضية جديدة في وقت التشغيل. هذه ليست مصادفة ، لأنه لا توجد مصادفات: لقد استخدم مُصمم V8 للعمل على Java VM عالي الأداء .

نقل القاموس


هناك طريقة أخرى لتنفيذ واجهات ديناميكية وهي نقل جدول مع مؤشرات الوظائف المطلوبة إلى الوظيفة العامة التي تحتاج إليها. يشبه هذا إلى حد ما إنشاء كائنات واجهة Go-shaped في مكان الاستدعاء ، فقط في حالتنا يتم تمرير الجدول كحجة مخفية ، ولا يتم تجميعه في حزمة كأحد الوسيطات الحالية.

يتم استخدام هذا النهج في فصول الكتابة في Haskell ، على الرغم من أن GHC يسمح لك بإجراء نوع من التوحيد باستخدام التضمين والتخصص. يستخدم OCaml عملية نقل القاموس مع حجة صريحة في شكل وحدات من الدرجة الأولى ، ولكن تم بالفعل اقتراح إضافة القدرة على جعل المعلمة ضمنية .

جداول الشهود في سويفت


استخدم مؤلفو Swift حلاً مثيرًا للاهتمام: نقل القاموس ، وكذلك وضع البيانات على أحجام الكتابة وكيفية نقلها ونسخها وإطلاقها في الجدول ، يتيح لك توفير جميع المعلومات اللازمة للعمل الموحد بأي أنواع دون تعبئتها. وهكذا ، يمكن لـ Swift تنفيذ الأدوية العامة دون تشكيلها ووضعها في الذاكرة في تمثيل موحد لجميع الكيانات! نعم ، يجب أن تدفع ثمن عمليات البحث الديناميكية ، كما هي الحال بالنسبة لكل أفراد العائلة الذين يستخدمون التغليف ، لكنه يوفر الموارد لوضعها في الذاكرة واستهلاكها وتعارضها في ذاكرة التخزين المؤقت. باستخدام الوظائف الموضحة على أنهاinlinable ، فإن برنامج التحويل البرمجي لـ Swift قادر أيضًا على التخصص (المونورفور) والأدوية المضمنة داخل الوحدة النمطية أو بين الوحدات النمطية لتجنب النفقات المذكورة. ربما يتم استخدام تقييم إرشادي للتأثير على حجم الرمز.

يوضح هذا أيضًا كيف يمكن لـ Swift تنفيذ استقرار ABI ، مع السماح لك بإضافة الحقول وإعادة توزيعها في البنية ، على الرغم من أن المؤلفين يوفرون السمة @frozen لرفض عمليات البحث الديناميكية للحصول على أداء أفضل.

تحليل النوع المكثف


هناك طريقة أخرى لتنفيذ واجهات لأنواع حزم. نضيف معرف النوع إلى جزء معين من الكائن ، باتباع مثال مؤشر vtable ، ثم نقوم بإنشاء وظائف لكل طريقة واجهة بها تعبير switch كبير لجميع الأنواع التي تنفذ هذه الطريقة ونمررها إلى الطريقة الصحيحة للنوع المحددة.

لا أحذر من استخدام اللغات التي تستخدم هذا الأسلوب ، لكن برامج التحويل البرمجي لـ C ++ وأجهزة Java الافتراضية تعمل بطريقة مماثلة ، عند استخدام التحسين استنادًا إلى الملفات الشخصية ، يكتشفون أن مكانًا معينًا من استدعاء الوراثة يعمل في الغالب مع كائنات من أنواع معينة. يستبدل المترجمون و VMs نقاط الاتصال بشيكات لكل نوع عادي ، ثم يرسلون هذه الأنواع بشكل ثابت ، كاحتياطي باستخدام الإرسال الديناميكي التقليدي. لذلك ، يمكن لخوارزمية تنبؤ الفرع أن تتنبأ بالفرع الذي سيتم تشغيل الكود ، وستواصل إرسال التعليمات باستخدام مكالمات ثابتة.

Monomorfizatsiya


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

توليد شفرة المصدر


أسهل طريقة للتحليل هي النسخ في مرحلة العرض التقديمي الأول: نسخ شفرة المصدر! ومن ثم ، لا يتعين على المترجم دعم الوراثة ، ويتم ذلك في بعض الأحيان من قبل مستخدمي اللغتين C و Go ، والذين لا يمتلك المترجمون هذا الدعم.

في C ، يمكنك استخدام معالج مسبق وتحديد بنية البيانات في ماكرو أو رأس عن طريق إدراجه مرارًا وتكرارًا باستخدام #define مختلف. Go بها نصوص مثل genny تجعل من السهل إنشاء كود.

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

سلسلة الخلطات في د


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

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

وحدات الماكرو الإجرائية الصدأ


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

بناء جملة شجرة وحدات الماكرو


تذهب بعض اللغات إلى أبعد من ذلك وتقدم أدوات لاستخدام وإنشاء أنواع مختلفة من أشجار بناء الجملة المجردة في وحدات الماكرو (Abstract Syntax Tree، AST). تتضمن الأمثلة Template Haskell و Nim macros و OCaml PPX و Lisp كلها تقريبًا.

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

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

 -- using AST construction functions genFn :: Name -> Q Exp genFn f = do x <- newName "x" lamE [varP x] (appE (varE f) (varE x)) -- using quotation with $() for splicing genFn' :: Name -> Q Exp genFn' f = [| \x -> $(varE f) x |] 

, , , . . , PPX OCaml / , . Rust , parsing quotation , , . Rust , , !

قوالب


— . ++ D , « ». , , , , .

 template <class T> T myMax(T a, T b) { return (a>b?a:b); } template <class T> struct Pair { T values[2]; }; int main() { myMax(5, 6); Pair<int> p { {5,6} }; // This would give us a compile error inside myMax // about Pair being an invalid operand to `>`: // myMax(p, p); } 

, , . , , . D , , : , , . D; if ( ! ):

 // We're going to use the isNumeric function in std.traits import std.traits; // The `if` is optional (without it you'll get an error inside like C++) // The `if` is also included in docs and participates in overloading! T myMax(T)(T a, T b) if(isNumeric!T) { return (a>b?a:b); } struct Pair(T) { T[2] values; } void main() { myMax(5, 6); Pair!int p = {[5,6]}; // This would give a compile error saying that `(Pair!int, Pair!int)` // doesn't match the available instance `myMax(T a, T b) if(isNumeric!T)`: // myMax(p, p); } 

C++20 «» , , .


D , (compile time function evaluation) static if , , , , - runtime-. , , , ++ , .

, « ». , Zig:

 fn Stack(comptime T: type) type { return struct { items: []T, len: usize, const Self = @This(); pub fn push(self: Self, item: T) { // ... } }; } 

Zig , , comptime . Terra , . Terra — Lua, - , Lua API , quoting splicing:

 function MakeStack(T) local struct Stack { items : &T; -- &T is a pointer to T len : int; } terra Stack:push(item : T) -- ... end return Stack end 

Terra - (domain specific) , Java Go . Terra runtime , .

Rust


, . , , ++, . , , , , . , . Rust, — Swift Haskell.

Rust « » (trait bounds). Trait — , , . Rust , - , , , . - Rust . , -.

 fn my_max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } } struct Pair<T> { values: [T; 2], } fn main() { my_max(5,6); let p: Pair<i32> = Pair { values: [5,6] }; // Would give a compile error saying that // PartialOrd is not implemented for Pair<i32>: // my_max(p,p); } 

, . Rust . Rust 2018 , v: &impl SomeTrait , v: &dyn SomeTrait . GHC Swift Haskell , .


— , . , (placeholders) -, , . memcpy , ! , . . JIT, , .

, , , , , , ! , , , . , , .

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


All Articles