प्रोग्रामर लगातार इस बात पर बहस कर रहे हैं कि कौन सी भाषा सबसे अच्छी है। एक बार हमने सी और पास्कल की तुलना की, लेकिन समय बीत गया। पायथन / रूबी और जावा /सी# की लड़ाई पहले से ही हमारे पीछे है। प्रत्येक भाषा के अपने फायदे और नुकसान हैं, यही कारण है कि हम उनकी तुलना करते हैं। आदर्श रूप से, हम अपनी आवश्यकताओं के अनुरूप भाषाओं का विस्तार करना चाहेंगे। प्रोग्रामर्स के पास यह अवसर बहुत लंबे समय से है। हम मेटाप्रोग्रामिंग के विभिन्न तरीकों को जानते हैं, यानी प्रोग्राम बनाने के लिए प्रोग्राम बनाना। यहां तक कि सी में मामूली मैक्रोज़ भी आपको छोटे विवरणों से कोड के बड़े हिस्से उत्पन्न करने की अनुमति देते हैं। हालाँकि, ये मैक्रोज़ अविश्वसनीय, सीमित और बहुत अभिव्यंजक नहीं हैं। आधुनिक भाषाओं में विस्तार के अधिक अभिव्यंजक साधन हैं। इन्हीं भाषाओं में से एक है कोटलिन।
एक डोमेन-विशिष्ट भाषा (डीएसएल) एक ऐसी भाषा है जो जावा, सी#, सी++ और अन्य जैसी सामान्य-उद्देश्य वाली भाषाओं के विपरीत, विशेष रूप से एक विशिष्ट विषय क्षेत्र के लिए विकसित की जाती है। इसका मतलब यह है कि विषय क्षेत्र के कार्यों का वर्णन करना आसान, अधिक सुविधाजनक और अधिक अभिव्यंजक है, लेकिन साथ ही यह रोजमर्रा के कार्यों को हल करने के लिए असुविधाजनक और अव्यवहारिक है, अर्थात यह एक सार्वभौमिक भाषा नहीं है। उदाहरण के तौर पर डीएसएल, आप रेगुलर एक्सप्रेशन भाषा ले सकते हैं। रेगुलर एक्सप्रेशन का विषय क्षेत्र स्ट्रिंग्स का प्रारूप है।
प्रारूप के अनुपालन के लिए स्ट्रिंग की जांच करने के लिए, बस एक लाइब्रेरी का उपयोग करना पर्याप्त है जो नियमित अभिव्यक्तियों के लिए समर्थन लागू करता है:
private boolean isIdentifierOrInteger(String s) { return s.matches("^\\s*(\\w+\\d*|\\d+)$"); }
यदि आप सार्वभौमिक भाषा, उदाहरण के लिए, जावा में निर्दिष्ट प्रारूप के अनुपालन के लिए स्ट्रिंग की जांच करते हैं, तो आपको निम्नलिखित कोड मिलेगा:
private boolean isIdentifierOrInteger(String s) { int index = 0; while (index < s.length() && isSpaceChar(s.charAt(index))) { index++; } if (index == s.length()) { return false; } if (isLetter(s.charAt(index))) { index++; while (index < s.length() && isLetter(s.charAt(index))) index++; while (index < s.length() && isDigit(s.charAt(index))) index++; } else if (Character.isDigit(s.charAt(index))) { while (index < s.length() && isDigit(s.charAt(index))) index++; } return index == s.length(); }
उपरोक्त कोड को नियमित अभिव्यक्तियों की तुलना में पढ़ना कठिन है, गलतियाँ करना आसान है और परिवर्तन करना अधिक कठिन है।
डीएसएल के अन्य सामान्य उदाहरण HTML, CSS, SQL, UML और BPMN हैं (बाद वाले दो ग्राफिकल नोटेशन का उपयोग करते हैं)। डीएसएल का उपयोग न केवल डेवलपर्स द्वारा बल्कि परीक्षकों और गैर-आईटी विशेषज्ञों द्वारा भी किया जाता है।
डीएसएल को दो प्रकारों में विभाजित किया गया है: बाहरी और आंतरिक। बाहरी डीएसएल भाषाओं का अपना वाक्यविन्यास होता है और वे उस सार्वभौमिक प्रोग्रामिंग भाषा पर निर्भर नहीं होते हैं जिसमें उनका समर्थन लागू किया जाता है।
बाहरी डीएसएल के फायदे और नुकसान:
🟢 विभिन्न भाषाओं/तैयार पुस्तकालयों में कोड जनरेशन
🟢 अपना सिंटैक्स सेट करने के लिए अधिक विकल्प
🔴 विशेष उपकरणों का उपयोग: एएनटीएलआर, वाईएसीसी, लेक्स
🔴 कभी-कभी व्याकरण का वर्णन करना कठिन होता है
🔴 कोई आईडीई समर्थन नहीं है, आपको अपना प्लगइन लिखना होगा
आंतरिक डीएसएल एक विशिष्ट सार्वभौमिक प्रोग्रामिंग भाषा (होस्ट भाषा) पर आधारित होते हैं। अर्थात्, मेजबान भाषा के मानक उपकरणों की सहायता से पुस्तकालय बनाए जाते हैं जो आपको अधिक संक्षिप्त रूप से लिखने की अनुमति देते हैं। उदाहरण के तौर पर, फ़्लुएंट एपीआई दृष्टिकोण पर विचार करें।
आंतरिक डीएसएल के फायदे और नुकसान:
🟢 मेजबान भाषा के भावों को आधार के रूप में उपयोग करता है
🟢 होस्ट भाषाओं में कोड में डीएसएल एम्बेड करना आसान है और इसके विपरीत
🟢 कोड जनरेशन की आवश्यकता नहीं है
🟢 होस्ट भाषा में सबरूटीन के रूप में डिबग किया जा सकता है
🔴 वाक्यविन्यास सेटिंग में सीमित संभावनाएँ
हाल ही में, कंपनी में हमें अपना डीएसएल बनाने की आवश्यकता का सामना करना पड़ा। हमारे उत्पाद ने खरीद स्वीकृति की कार्यक्षमता लागू कर दी है। यह मॉड्यूल BPM (बिजनेस प्रोसेस मैनेजमेंट) का एक मिनी इंजन है। व्यावसायिक प्रक्रियाओं को अक्सर ग्राफ़िक रूप से दर्शाया जाता है। उदाहरण के लिए, नीचे दिया गया बीपीएमएन नोटेशन एक प्रक्रिया दिखाता है जिसमें टास्क 1 को निष्पादित करना और फिर टास्क 2 और टास्क 3 को समानांतर में निष्पादित करना शामिल है।
हमारे लिए व्यावसायिक प्रक्रियाओं को प्रोग्रामेटिक रूप से बनाने में सक्षम होना महत्वपूर्ण था, जिसमें गतिशील रूप से एक मार्ग बनाना, अनुमोदन चरणों के लिए कलाकारों को सेट करना, चरण निष्पादन के लिए समय सीमा निर्धारित करना आदि शामिल था। ऐसा करने के लिए, हमने पहले फ़्लुएंट एपीआई का उपयोग करके इस समस्या को हल करने का प्रयास किया। दृष्टिकोण।
फिर हमने निष्कर्ष निकाला कि फ़्लुएंट एपीआई का उपयोग करके स्वीकृति मार्ग सेट करना अभी भी बोझिल हो गया है और हमारी टीम ने अपना स्वयं का डीएसएल बनाने के विकल्प पर विचार किया। हमने जांच की कि कोटलिन पर आधारित बाहरी डीएसएल और आंतरिक डीएसएल पर स्वीकृति मार्ग कैसा दिखेगा (क्योंकि हमारा उत्पाद कोड जावा और कोटलिन में लिखा गया है)।
बाहरी डीएसएल:
acceptance addStep executor: HEAD_OF_DEPARTMENT duration: 7 days protocol should be formed parallel addStep executor: FINANCE_DEPARTMENT or CTO or CEO condition: ${!request.isInternal} duration: 7 work days after start date addStep executor: CTO dueDate: 2022-12-08 08:00 PST can change addStep executor: SECRETARY protocol should be signed
आंतरिक डीएसएल:
acceptance { addStep { executor = HEAD_OF_DEPARTMENT duration = days(7) protocol shouldBe formed } parallel { addStep { executor = FINANCE_DEPARTMENT or CTO or CEO condition = !request.isInternal duration = startDate() + workDays(7) } addStep { executor = CTO dueDate = "2022-12-08 08:00" timezone PST +canChange } } addStep { executor = SECRETARY protocol shouldBe signed } }
घुंघराले कोष्ठक को छोड़कर, दोनों विकल्प लगभग समान हैं। इसलिए, यह निर्णय लिया गया कि बाहरी डीएसएल विकसित करने में समय और प्रयास बर्बाद न किया जाए, बल्कि एक आंतरिक डीएसएल बनाया जाए।
आइए एक ऑब्जेक्ट मॉडल विकसित करना शुरू करें
interface AcceptanceElement class StepContext : AcceptanceElement { lateinit var executor: ExecutorCondition var duration: Duration? = null var dueDate: ZonedDateTime? = null val protocol = Protocol() var condition = true var canChange = ChangePermission() } class AcceptanceContext : AcceptanceElement { val elements = mutableListOf<AcceptanceElement>() fun addStep(init: StepContext.() -> Unit) { elements += StepContext().apply(init) } fun parallel(init: AcceptanceContext.() -> Unit) { elements += AcceptanceContext().apply(init) } } object acceptance { operator fun invoke(init: AcceptanceContext.() -> Unit): AcceptanceContext { val acceptanceContext = AcceptanceContext() acceptanceContext.init() return acceptanceContext } }
सबसे पहले, आइए AcceptanceContext
क्लास को देखें। इसे मार्ग तत्वों के संग्रह को संग्रहीत करने के लिए डिज़ाइन किया गया है और इसका उपयोग संपूर्ण आरेख के साथ-साथ parallel
-ब्लॉकों का प्रतिनिधित्व करने के लिए किया जाता है।
addStep
और parallel
विधियाँ एक पैरामीटर के रूप में रिसीवर के साथ एक लैम्ब्डा लेती हैं।
एक रिसीवर के साथ एक लैम्ब्डा एक लैम्ब्डा अभिव्यक्ति को परिभाषित करने का एक तरीका है जिसकी एक विशिष्ट रिसीवर ऑब्जेक्ट तक पहुंच होती है। फ़ंक्शन लिटरल के मुख्य भाग के अंदर, किसी कॉल में पास किया गया रिसीवर ऑब्जेक्ट एक अंतर्निहित this
बन जाता है, ताकि आप बिना किसी अतिरिक्त क्वालीफायर के उस रिसीवर ऑब्जेक्ट के सदस्यों तक पहुंच सकें, या this
अभिव्यक्ति का उपयोग करके रिसीवर ऑब्जेक्ट तक पहुंच सकें।
साथ ही, यदि किसी विधि कॉल का अंतिम तर्क लैम्ब्डा है, तो लैम्ब्डा को कोष्ठक के बाहर रखा जा सकता है। इसीलिए हम अपने डीएसएल में निम्नलिखित जैसा एक कोड लिख सकते हैं:
parallel { addStep { executor = FINANCE_DEPARTMENT ... } addStep { executor = CTO ... } }
यह सिंटैक्टिक शुगर के बिना एक कोड के बराबर है:
parallel({ this.addStep({ this.executor = FINANCE_DEPARTMENT ... }) this.addStep({ this.executor = CTO ... }) })
रिसीवर्स के साथ लैम्ब्डा और कोष्ठक के बाहर लैम्ब्डा कोटलिन विशेषताएं हैं जो डीएसएल के साथ काम करते समय विशेष रूप से उपयोगी होती हैं।
अब आइए इकाई acceptance
पर नजर डालें। acceptance
एक वस्तु है. कोटलिन में, एक ऑब्जेक्ट घोषणा एक सिंगलटन को परिभाषित करने का एक तरीका है - केवल एक उदाहरण वाला एक वर्ग। तो, ऑब्जेक्ट घोषणा एक ही समय में वर्ग और उसके एकल उदाहरण दोनों को परिभाषित करती है।
इसके अलावा, invoke
ऑपरेटर accreditation
वस्तु के लिए अतिभारित है। invoke
ऑपरेटर एक विशेष फ़ंक्शन है जिसे आप अपनी कक्षाओं में परिभाषित कर सकते हैं। जब आप किसी क्लास के इंस्टेंस को ऐसे इनवोक करते हैं जैसे कि वह कोई फ़ंक्शन हो, तो invoke
ऑपरेटर फ़ंक्शन को कॉल किया जाता है। यह आपको वस्तुओं को फ़ंक्शन के रूप में मानने और उन्हें फ़ंक्शन-जैसी तरीके से कॉल करने की अनुमति देता है।
ध्यान दें कि invoke
विधि का पैरामीटर भी एक रिसीवर के साथ एक लैम्ब्डा है। अब हम एक स्वीकृति मार्ग परिभाषित कर सकते हैं...
val acceptanceRoute = acceptance { addStep { executor = HEAD_OF_DEPARTMENT ... } parallel { addStep { executor = FINANCE_DEPARTMENT ... } addStep { executor = CTO ... } } addStep { executor = SECRETARY ... } }
...और इसके माध्यम से चलो
val headOfDepartmentStep = acceptanceRoute.elements[0] as StepContext val parallelBlock = acceptanceRoute.elements[1] as AcceptanceContext val ctoStep = parallelBlock.elements[1] as StepContext
इस कोड पर एक नजर डालें
addStep { executor = FINANCE_DEPARTMENT or CTO or CEO ... }
हम इसे निम्नलिखित द्वारा कार्यान्वित कर सकते हैं:
enum class ExecutorConditionType { EQUALS, OR } data class ExecutorCondition( private val name: String, private val values: Set<ExecutorCondition>, private val type: ExecutorConditionType, ) { infix fun or(another: ExecutorCondition) = ExecutorCondition("or", setOf(this, another), ExecutorConditionType.OR) } val HEAD_OF_DEPARTMENT = ExecutorCondition("HEAD_OF_DEPARTMENT", setOf(), ExecutorConditionType.EQUALS) val FINANCE_DEPARTMENT = ExecutorCondition("FINANCE_DEPARTMENT", setOf(), ExecutorConditionType.EQUALS) val CHIEF = ExecutorCondition("CHIEF", setOf(), ExecutorConditionType.EQUALS) val CTO = ExecutorCondition("CTO", setOf(), ExecutorConditionType.EQUALS) val SECRETARY = ExecutorCondition("SECRETARY", setOf(), ExecutorConditionType.EQUALS)
ExecutorCondition
वर्ग हमें कई संभावित कार्य निष्पादकों को सेट करने की अनुमति देता है। ExecutorCondition
में हम infix फ़ंक्शन or
परिभाषित करते हैं। इन्फिक्स फ़ंक्शन एक विशेष प्रकार का फ़ंक्शन है जो आपको अधिक प्राकृतिक, इन्फिक्स नोटेशन का उपयोग करके इसे कॉल करने की अनुमति देता है।
भाषा की इस विशेषता का उपयोग किए बिना, हमें इस प्रकार लिखना होगा:
addStep { executor = FINANCE_DEPARTMENT.or(CTO).or(CEO) ... }
इन्फिक्स फ़ंक्शंस का उपयोग प्रोटोकॉल की आवश्यक स्थिति और समयक्षेत्र के साथ समय निर्धारित करने के लिए भी किया जाता है।
enum class ProtocolState { formed, signed } class Protocol { var state: ProtocolState? = null infix fun shouldBe(state: ProtocolState) { this.state = state } } enum class TimeZone { ... PST, ... } infix fun String.timezone(tz: TimeZone): ZonedDateTime { val format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z") return ZonedDateTime.parse("$this $tz", format) }
String.timezone
एक एक्सटेंशन फ़ंक्शन है। कोटलिन में, एक्सटेंशन फ़ंक्शन आपको मौजूदा कक्षाओं में उनके स्रोत कोड को संशोधित किए बिना नए फ़ंक्शन जोड़ने की अनुमति देते हैं। यह सुविधा विशेष रूप से तब उपयोगी होती है जब आप उन कक्षाओं की कार्यक्षमता को बढ़ाना चाहते हैं जिन पर आपका नियंत्रण नहीं है, जैसे मानक या बाहरी पुस्तकालयों की कक्षाएं।
डीएसएल में उपयोग:
addStep { ... protocol shouldBe formed dueDate = "2022-12-08 08:00" timezone PST ... }
यहां "2022-12-08 08:00"
एक रिसीवर ऑब्जेक्ट है, जिस पर एक्सटेंशन फ़ंक्शन timezone
कहा जाता है, और PST
पैरामीटर है। this
कीवर्ड का उपयोग करके रिसीवर ऑब्जेक्ट तक पहुंच प्राप्त की जाती है।
अगली कोटलिन सुविधा जो हम अपने डीएसएल में उपयोग करते हैं वह ऑपरेटर ओवरलोडिंग है। हम पहले ही invoke
ऑपरेटर के अधिभार पर विचार कर चुके हैं। कोटलिन में, आप अंकगणित सहित अन्य ऑपरेटरों को अधिभारित कर सकते हैं।
addStep { ... +canChange }
यहां यूनरी ऑपरेटर +
अतिभारित है। नीचे वह कोड है जो इसे लागू करता है:
class StepContext : AcceptanceElement { ... var canChange = ChangePermission() } data class ChangePermission( var canChange: Boolean = true, ) { operator fun unaryPlus() { canChange = true } operator fun unaryMinus() { canChange = false } }
अब हम अपने डीएसएल पर स्वीकृति मार्गों का वर्णन कर सकते हैं। हालाँकि, DSL उपयोगकर्ताओं को संभावित त्रुटियों से बचाया जाना चाहिए। उदाहरण के लिए, वर्तमान संस्करण में, निम्नलिखित कोड स्वीकार्य है:
val acceptanceRoute = acceptance { addStep { executor = HEAD_OF_DEPARTMENT duration = days(7) protocol shouldBe signed addStep { executor = FINANCE_DEPARTMENT } } }
addStep
के भीतर addStep
अजीब लगता है, है ना? आइए जानें कि यह कोड बिना किसी त्रुटि के सफलतापूर्वक संकलित क्यों होता है। जैसा कि ऊपर बताया गया है, acceptance#invoke
और AcceptanceContext#addStep
विधियां एक पैरामीटर के रूप में एक रिसीवर के साथ एक लैम्ब्डा लेती हैं, और एक रिसीवर ऑब्जेक्ट को this
कीवर्ड द्वारा एक्सेस किया जा सकता है। तो हम पिछले कोड को इस तरह फिर से लिख सकते हैं:
val acceptanceRoute = acceptance { [email protected] { [email protected] = HEAD_OF_DEPARTMENT [email protected] = days(7) [email protected] shouldBe signed [email protected] { executor = FINANCE_DEPARTMENT } } }
अब आप देख सकते हैं कि [email protected]
दोनों बार कॉल किया जाता है। विशेष रूप से ऐसे मामलों के लिए, कोटलिन के पास DslMarker
एनोटेशन है। आप कस्टम एनोटेशन परिभाषित करने के लिए @DslMarker
उपयोग कर सकते हैं। समान एनोटेशन से चिह्नित रिसीवरों को एक दूसरे के अंदर नहीं पहुँचा जा सकता है।
@DslMarker annotation class AcceptanceDslMarker @AcceptanceDslMarker class AcceptanceContext : AcceptanceElement { ... } @AcceptanceDslMarker class StepContext : AcceptanceElement { ... }
अब कोड
val acceptanceRoute = acceptance { addStep { ... addStep { ... } } }
'fun addStep(init: StepContext.() -> Unit): Unit' can't be called in this context by implicit receiver. Use the explicit one if necessary
इस लेख में जिन भाषा विशेषताओं पर विचार किया गया था, उन पर आधिकारिक कोटलिन दस्तावेज़ के लिंक नीचे दिए गए हैं:
डोमेन-विशिष्ट भाषाएँ किसी विशिष्ट डोमेन के भीतर समस्याओं को मॉडल करने और हल करने के लिए एक विशेष और अभिव्यंजक तरीका प्रदान करके उत्पादकता बढ़ाने, त्रुटियों को कम करने और सहयोग में सुधार करने का एक शक्तिशाली साधन प्रदान करती हैं। कोटलिन कई विशेषताओं और वाक्यात्मक शर्करा के साथ एक आधुनिक प्रोग्रामिंग भाषा है, इसलिए यह आंतरिक डीएसएल के लिए मेजबान भाषा के रूप में बहुत अच्छी है।