هذه هي ترجمة مقدمة إلى البرمجة الموجهة للسياق في كوتلينسأحاول في هذه المقالة وصف ظاهرة جديدة نشأت كنتيجة ثانوية للتطور السريع للغة Kotlin. هذا هو نهج جديد لتصميم بنية التطبيقات والمكتبات ، والتي سوف أسميها البرمجة الموجهة للسياق.
بضع كلمات حول أذونات الوظيفة
كما هو معروف ، هناك ثلاثة نماذج برمجة رئيسية (
ملاحظة Pedant : هناك نماذج أخرى):
- البرمجة الإجرائية
- وجوه المنحى البرمجة
- البرمجة الوظيفية
كل هذه الطرق تعمل مع وظائف بطريقة أو بأخرى. دعونا ننظر إلى هذا من وجهة نظر دقة الوظائف ، أو جدولة مكالماتهم (وهذا يعني اختيار الوظيفة التي ينبغي استخدامها في هذا المكان). تتميز البرمجة الإجرائية باستخدام الدالتين العموميتين ودقة استقراريهما استنادًا إلى اسم الدالة وأنواع الوسيطة. بالطبع ، يمكن استخدام الأنواع فقط في حالة اللغات المكتوبة بشكل ثابت. على سبيل المثال ، في Python ، يتم استدعاء الوظائف بالاسم ، وإذا كانت الوسيطات غير صحيحة ، يتم طرح استثناء في وقت التشغيل أثناء تنفيذ البرنامج. يعتمد حل الوظائف باللغات ذات النهج الإجرائي فقط على اسم الإجراء / الوظيفة ومعلماتها ، ويتم في معظم الحالات بشكل ثابت.
يحد نمط البرمجة الموجهة للكائن من نطاق الوظائف. الوظائف ليست عالمية ، بل هي جزء من الفصول الدراسية ، ولا يمكن استدعاؤها إلا في مثيل للفئة المقابلة (
ملاحظة Pedant : بعض اللغات الإجرائية الكلاسيكية لديها نظام معياري ، وبالتالي ، نطاق ؛ اللغة الإجرائية! = C).
بالطبع ، يمكننا دائمًا استبدال وظيفة عضو في فئة بوظيفة عمومية مع وسيطة إضافية لنوع الكائن المدعو ، ولكن من وجهة نظر تركيبية ، يكون الفرق كبيرًا. على سبيل المثال ، في هذه الحالة ، يتم تجميع الأساليب في الفصل الذي تشير إليه ، وبالتالي يصبح من الواضح بشكل أوضح نوع السلوك الذي توفره الكائنات من هذا النوع.
بطبيعة الحال ، فإن التغليف هو الأكثر أهمية هنا ، حيث يمكن أن تكون بعض الحقول من فئة أو سلوكها خاصة ويمكن الوصول إليها فقط لأعضاء هذه الفئة (لا يمكنك تقديم هذا في نهج إجرائي بحت) ، وتعدد الأشكال ، وذلك بفضل الطريقة المستخدمة بالفعل لا يتم تحديدها بناءً على الاسم الطريقة ، ولكن أيضا بناء على نوع الكائن الذي يطلق عليه. يعتمد إرسال استدعاء أسلوب في أسلوب موجه للكائن على نوع الكائن المحدد في وقت التشغيل ، واسم الطريقة ، ونوع الوسائط في مرحلة الترجمة.
النهج الوظيفي لا يجلب أي شيء جديد بشكل أساسي من حيث دقة الوظيفة. عادةً ما يكون للغات الموجهة للوظيفة قواعد أفضل للتمييز بين مناطق الرؤية (
ملاحظة المتحذلق : مرة أخرى ، C ليست كل اللغات الإجرائية ، فهناك تلك اللغات التي يتم فيها تحديد مناطق الرؤية بشكل جيد) والتي تسمح بمزيد من التحكم الدقيق في وضوح الوظائف استنادًا إلى النظام الوحدات النمطية ، ولكن بخلاف ذلك ، يتم القرار في وقت التحويل البرمجي استنادًا إلى نوع الوسائط.
ما هذا؟
في حالة منهج الكائن ، عند استدعاء طريقة على كائن ما ، لدينا حججها ، ولكن بالإضافة إلى ذلك لدينا معلمة صريحة (في حالة Python) أو معلمة ضمنية تمثل مثيلًا للفئة المدعوة (تتم فيما يلي كتابة جميع الأمثلة في Kotlin):
class A{ fun doSomething(){ println(" $this") } }
الطبقات المتداخلة والإغلاقات تعقد الأمور قليلاً:
interface B{ fun doBSomething() } class A{ fun doASomething(){ val b = object: B{ override fun doBSomething(){ println(" $this ${this@A}") } } b.doBSomething() } }
في هذه الحالة ، يوجد اثنان ضمنيًا
لدالة doBomething - أحدهما يتوافق مع مثيل الفئة
B ، والآخر ينشأ من إغلاق المثيل
A. يحدث الشيء نفسه في حالة إغلاق lambda الأكثر شيوعًا. من المهم ملاحظة أن هذا في هذه الحالة لا يعمل كمعلمة ضمنية فحسب ، بل أيضًا كنطاق أو سياق لجميع الوظائف والكائنات المدعوة في النطاق المعجمي. لذا فإن طريقة doBomething لديها حق الوصول إلى أي من أعضاء الفئة
A ، عامة أو خاصة ، وكذلك أعضاء
B نفسها.
وهنا هو Kotlin
تعطينا Kotlin
وظائف تمديد لعبة جديدة تمامًا. (
ملاحظة من Pedant : في الواقع ، ليست جديدة ، فهي موجودة أيضًا في C #). يمكنك تحديد وظيفة مثل
A.doASomething () في أي مكان في البرنامج ، وليس فقط داخل
A. داخل هذه الوظيفة ، لدينا
هذه المعلمة الضمنية ، تسمى المستقبِل ، مشيرة إلى المثال "
أ" التي تسمى الطريقة:
class A fun A.doASomthing(){ println(" - $this") } fun main(){ val a = A() a.doASomthing() }
لا تتمتع وظائف الامتداد بالوصول إلى الأعضاء الخاصين في المستلم ، لذلك لا يتم انتهاك التغليف.
الشيء المهم التالي الذي لدى Kotlin هو كتل الشفرة مع أجهزة الاستقبال. يمكنك تشغيل كتلة تعسفية من التعليمات البرمجية باستخدام شيء كمستلم:
class A{ fun doInternalSomething(){} } fun A.doASomthing(){} fun main(){ val a = A() with(a){ doInternalSomething() doASomthing() } }
في هذا المثال ، يمكن استدعاء كلتا الوظيفتين بدون "
a " إضافية في البداية ، لأن الدالة with تضع كل رمز الكتلة اللاحقة داخل سياق a. هذا يعني أن جميع الوظائف في هذه الكتلة تسمى كما لو كانت قد استُدعيت على الكائن (تم تمريره بشكل صريح)
الخطوة الأخيرة في هذه المرحلة في البرمجة الموجهة للسياق هي القدرة على إعلان الامتدادات كأعضاء في الفصل. في هذه الحالة ، يتم تعريف وظيفة التمديد داخل فئة أخرى ، مثل هذا:
class B class A{ fun B.doBSomething(){} } fun main(){ val a = A() val b = B() with(a){ b.doBSomething()
من المهم أن يحصل
B هنا على بعض السلوكيات الجديدة ، ولكن فقط عندما يكون في سياق معجمي محدد. دالة الامتداد هي عضو عادي في الفئة
A. هذا يعني أن دقة الوظيفة تتم بشكل ثابت بناءً على السياق الذي يتم استدعاؤها ، ولكن التنفيذ الحقيقي يتم تحديده بواسطة مثيل
A الذي يتم تمريره كسياق. وظيفة يمكن أن تتفاعل حتى مع حالة الكائن
أ .
الإرسال الموجه للسياق
في بداية المقال ، ناقشنا الأساليب المختلفة لإرسال استدعاءات الوظائف ، وقد تم ذلك لسبب ما. والحقيقة هي أن وظائف التمديد في Kotlin تسمح لك بالعمل مع إيفاد بطريقة جديدة. الآن القرار بشأن الوظيفة المعينة التي ينبغي استخدامها لا يعتمد فقط على نوع المعلمات الخاصة بها ، ولكن أيضًا على السياق المعجمى لدعوتها. أي أن التعبير نفسه في سياقات مختلفة يمكن أن يكون له معان مختلفة. بالطبع ، لا يتغير أي شيء من وجهة نظر التنفيذ ، ولا يزال لدينا كائن مستلم صريح يحدد الإرسال لطرقه وملحقاته الموصوفة في النص الأساسي للفئة نفسها (امتدادات الأعضاء) - ولكن من وجهة نظر بناء الجملة ، فهذه طريقة مختلفة .
دعونا نلقي نظرة على كيف يختلف النهج الموجه للسياق عن النهج الكلاسيكي المنحى ، باستخدام المشكلة الكلاسيكية المتمثلة في العمليات الحسابية على الأرقام في جافا كمثال. فئة
الأرقام في Java و Kotlin هي الأصل لجميع الأرقام ، ولكن على عكس الأرقام المتخصصة مثل Double ، فإنها لا تحدد عملياتها الرياضية. لذلك لا يمكنك الكتابة ، على سبيل المثال ، مثل هذا:
val n: Number = 1.0 n + 1.0
السبب هنا هو أنه لا يمكن تحديد العمليات الحسابية باستمرار لجميع الأنواع الرقمية. على سبيل المثال ، تقسيم عدد صحيح يختلف عن تقسيم النقطة العائمة. في بعض الحالات الخاصة ، يعرف المستخدم نوع العملية المطلوبة ، لكن عادة لا يكون من المنطقي تحديد مثل هذه الأشياء على المستوى العالمي. سيكون الحل الموجه للكائن (وفي الواقع ، وظيفي) هو تحديد نوع وريث جديد من فئة
الأرقام ، والعمليات اللازمة فيه ، واستخدامه عند الضرورة (في Kotlin 1.3 يمكنك استخدام الفئات المضمنة). بدلاً من ذلك ، دعونا نحدد سياقًا بهذه العمليات ونطبقه محليًا:
interface NumberOperations{ operator fun Number.plus(other: Number) : Number operator fun Number.minus(other: Number) : Number operator fun Number.times(other: Number) : Number operator fun Number.div(other: Number) : Number } object DoubleOperations: NumberOperations{ override fun Number.plus(other: Number) = this.toDouble() + other.toDouble() override fun Number.minus(other: Number) = this.toDouble() - other.toDouble() override fun Number.times(other: Number) = this.toDouble() * other.toDouble() override fun Number.div(other: Number) = this.toDouble() / other.toDouble() } fun main(){ val n1: Number = 1.0 val n2: Number = 2 val res = with(DoubleOperations){ (n1 + n2)/2 } println(res) }
في هذا المثال ، يتم حساب
res داخل سياق يحدد عمليات إضافية. ليس من الضروري تعريف السياق محليًا ؛ بدلاً من ذلك ، يمكن تمريره ضمنيًا كمستقبل دالة. على سبيل المثال ، يمكنك القيام بذلك:
fun NumberOperations.calculate(n1: Number, n2: Number) = (n1 + n2)/2 val res = DoubleOperations.calculate(n1, n2)
هذا يعني أن منطق العمليات داخل السياق منفصل تمامًا عن تنفيذ هذا السياق ، ويمكن كتابته في جزء آخر من البرنامج أو حتى في وحدة نمطية أخرى. في هذا المثال البسيط ، يكون السياق عبارة عن مفردة عديمة الحالة ، ولكن يمكن أيضًا استخدام سياقات الحالة.
تجدر الإشارة إلى أنه يمكن تداخل السياقات:
with(a){ with(b){ doSomething() } }
هذا يعطي تأثير الجمع بين سلوك كلتا الفئتين ، ومع ذلك ، من الصعب السيطرة على هذه الميزة اليوم بسبب نقص الامتدادات مع عدة مستلمين (
KT-10468 ).
قوة صريحة Coroutines
يتم استخدام أحد أفضل أمثلة النهج الموجه للسياق في مكتبة Kotlinx-coroutines. يمكن العثور على شرح لهذه الفكرة في
مقال بقلم رومان إليزاروف. هنا ، أود فقط التأكيد على أن
CoroutineScope هو حالة تصميم موجه للسياق مع سياق حالة. يلعب CoroutineScope دورين:
- أنه يحتوي على CoroutineContext ، وهو أمر مطلوب لتشغيل coroutine ويتم توارثه عندما يتم إطلاق coroutine جديد.
- أنه يحتوي على حالة coroutine الأصل ، والذي يسمح لك لإلغاء ذلك إذا ألقى coroutine ولدت خطأ.
يوفر أيضًا التزامن المنظم مثالًا رائعًا على بنية موجهة للسياق:
suspend fun CoroutineScope.doSomeWork(){} GlobalScope.launch{ launch{ delay(100) doSomeWork() } }
هنا ،
doSomeWork هي وظيفة سياق ، ولكنها محددة خارج سياقها. تنشئ طرق
الإطلاق سياحتين متداخلتين تعادل المساحات المعجمية للوظائف المناظرة (في هذه الحالة ، يكون كلا السياقين من نفس النوع ، وبالتالي فإن السياق الداخلي يحجب السياق الخارجي). نقطة الانطلاق الجيدة لتعلم كووتلن هو الدليل الرسمي.
DSL
هناك فئة واسعة من المهام بالنسبة إلى Kotlin ، والتي يشار إليها عادةً باسم مهام بناء DSL (لغة المجال المحددة). في هذه الحالة ، يُفهم DSL على أنه رمز يوفر أداة إنشاء سهلة الاستخدام من نوع ما من البنية المعقدة. في الواقع ، فإن استخدام مصطلح DSL ليس صحيحًا تمامًا هنا ، كما هو في مثل هذه الحالات ، يتم استخدام بناء جملة Kotlin الأساسي ببساطة دون أي حيل خاصة - لكن ما زلنا نستخدم هذا المصطلح الشائع.
بناة DSL موجهة للسياق في معظم الحالات. على سبيل المثال ، إذا كنت ترغب في إنشاء عنصر HTML ، فعليك أولاً التحقق مما إذا كان يمكن إضافة هذا العنصر المحدد إلى هذا المكان. تقوم مكتبة
kotlinx.html بذلك عن طريق توفير ملحقات فئة تستند إلى السياق وتمثل علامة معينة. في الواقع ، تتكون المكتبة بأكملها من امتدادات السياق لعناصر DOM الحالية.
مثال آخر هو
بناء واجهة المستخدم الرسومية TornadoFX . يتم ترتيب المنشئ الكامل للرسم البياني للمشهد كسلسلة من صانعي السياق المتداخل ، حيث تكون الكتل الداخلية مسؤولة عن بناء الأطفال للكتل الخارجية أو ضبط معلمات الوالدين. فيما يلي مثال من الوثائق الرسمية:
override val root = gridPane{ tabpane { gridpaneConstraints { vhGrow = Priority.ALWAYS } tab("Report", HBox()) { label("Report goes here") } tab("Data", GridPane()) { tableview<Person> { items = persons column("ID", Person::idProperty) column("Name", Person::nameProperty) column("Birthday", Person::birthdayProperty) column("Age", Person::ageProperty).cellFormat { if (it < 18) { style = "-fx-background-color:#8b0000; -fx-text-fill:white" text = it.toString() } else { text = it.toString() } } } } } }
في هذا المثال ، تحدد المنطقة المعجمية سياقها (وهو منطقي ، لأنه يمثل قسم واجهة المستخدم الرسومية وهيكلها الداخلي) ، وله حق الوصول إلى السياقات الأصل.
ما هو التالي: عدة مستلمين
توفر البرمجة الموجهة للسياق لمطوري Kotlin العديد من الأدوات ويفتح طريقة جديدة لتصميم بنية التطبيق. هل نحن بحاجة إلى أي شيء آخر؟ ربما نعم.
في الوقت الحالي ، يقتصر التطور في النهج السياقي على حقيقة أنك تحتاج إلى تعريف الامتدادات من أجل الحصول على نوع من السلوك الطبقي محدود السياق. هذا جيد عندما يتعلق الأمر بفصل مخصص ، لكن ماذا لو أردنا الشيء نفسه لفئة من مكتبة؟ أو إذا كنا نريد إنشاء امتداد لسلوك مقيد بالفعل (على سبيل المثال ، أضف نوعًا من الامتداد داخل CoroutineScope)؟ لا تسمح Kotlin حاليًا بامتداد وظائف الامتداد لأكثر من مستلم واحد. ولكن يمكن إضافة مستلمين متعددين إلى اللغة دون كسر التوافق مع الإصدارات السابقة. تتم مناقشة إمكانية استخدام عدة مستلمين (
KT-10468 ) وسيتم إصدارها
كطلب KEEP (UPD:
تم إصداره بالفعل ). مشكلة (أو ربما شريحة) من السياقات المتداخلة هي أنها تتيح لك تغطية معظم ، إن لم يكن كل ، خيارات استخدام فئات
الكتابة (
أنواع الكتابة ) ، آخر مرغوب فيه للغاية من الميزات المقترحة. من غير المحتمل أن يتم تطبيق كلتا الميزتين باللغة في نفس الوقت.
إضافة
نود أن نشكر صديقنا المتحمس لبيدان وهاسكل بدوام كامل
أليكسي خودياكوف على تعليقاته على نص المقالة والتعديلات على استخدامي المجاني للمصطلحات. أشكر أيضًا إيليا ريجينكوف على التعليقات القيمة وتصحيح النسخة الإنجليزية من المقال.
مؤلف المقال الأصلي:
ألكسندر نوزيك ، نائب رئيس
مختبر الأساليب الفيزيائية التجريبية في
أبحاث JetBrains .
ترجمة:
بيتر كليماي ، باحث
في مختبر تجارب الفيزياء النووية في
أبحاث JetBrains