প্রোগ্রামাররা ক্রমাগত তর্ক করছে কোন ভাষাটি সেরা তা নিয়ে। একবার আমরা C এবং Pascal তুলনা করেছি, কিন্তু সময় কেটে গেছে। পাইথন / রুবি এবং জাভা /সি# এর যুদ্ধগুলি ইতিমধ্যে আমাদের পিছনে রয়েছে। প্রতিটি ভাষার তার সুবিধা এবং অসুবিধা আছে, তাই আমরা তাদের তুলনা করি। আদর্শভাবে, আমরা আমাদের নিজস্ব প্রয়োজন অনুসারে ভাষাগুলিকে প্রসারিত করতে চাই। প্রোগ্রামারদের এই সুযোগটি অনেক দিন ধরেই ছিল। আমরা মেটাপ্রোগ্রামিং এর বিভিন্ন উপায় জানি, অর্থাৎ প্রোগ্রাম তৈরি করার জন্য প্রোগ্রাম তৈরি করা। এমনকি সি-তে তুচ্ছ ম্যাক্রোগুলি আপনাকে ছোট বিবরণ থেকে কোডের বড় অংশ তৈরি করতে দেয়। যাইহোক, এই ম্যাক্রোগুলি অবিশ্বস্ত, সীমিত এবং খুব অভিব্যক্তিপূর্ণ নয়। আধুনিক ভাষাগুলির সম্প্রসারণের অনেক বেশি অভিব্যক্তিপূর্ণ উপায় রয়েছে। এই ভাষাগুলির মধ্যে একটি হল কোটলিন।
একটি ডোমেন-নির্দিষ্ট ভাষা (DSL) হল এমন একটি ভাষা যা একটি নির্দিষ্ট বিষয়ের জন্য বিশেষভাবে তৈরি করা হয়, সাধারণ-উদ্দেশ্যের ভাষা যেমন জাভা, C#, C++ এবং অন্যান্যদের বিপরীতে। এর অর্থ হল বিষয় এলাকার কাজগুলি বর্ণনা করা সহজ, আরও সুবিধাজনক এবং আরও অভিব্যক্তিপূর্ণ, কিন্তু একই সময়ে এটি অসুবিধেজনক, এবং দৈনন্দিন কাজগুলি সমাধান করার জন্য অব্যবহারিক, অর্থাৎ এটি একটি সর্বজনীন ভাষা নয়৷ উদাহরণ হিসাবে DSL, আপনি রেগুলার এক্সপ্রেশন ভাষা নিতে পারেন। রেগুলার এক্সপ্রেশনের বিষয় ক্ষেত্র হল স্ট্রিং এর বিন্যাস।
বিন্যাসের সাথে সম্মতির জন্য স্ট্রিংটি পরীক্ষা করার জন্য, রেগুলার এক্সপ্রেশনের জন্য সমর্থন প্রয়োগ করে এমন একটি লাইব্রেরি ব্যবহার করাই যথেষ্ট:
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(); }
উপরের কোডটি নিয়মিত এক্সপ্রেশনের চেয়ে পড়া কঠিন, ভুল করা সহজ এবং পরিবর্তন করা আরও জটিল।
ডিএসএল-এর অন্যান্য সাধারণ উদাহরণ হল এইচটিএমএল, সিএসএস, এসকিউএল, ইউএমএল এবং বিপিএমএন (পরবর্তী দুটি গ্রাফিক্যাল নোটেশন ব্যবহার করে)। ডিএসএলগুলি শুধুমাত্র বিকাশকারীরা নয়, পরীক্ষক এবং অ-আইটি বিশেষজ্ঞদের দ্বারাও ব্যবহৃত হয়।
ডিএসএল দুটি প্রকারে বিভক্ত: বাহ্যিক এবং অভ্যন্তরীণ। বাহ্যিক DSL ভাষার নিজস্ব সিনট্যাক্স রয়েছে এবং তারা সার্বজনীন প্রোগ্রামিং ভাষার উপর নির্ভর করে না যেখানে তাদের সমর্থন বাস্তবায়িত হয়।
বাহ্যিক DSL-এর সুবিধা এবং অসুবিধা:
🟢 বিভিন্ন ভাষায় কোড জেনারেশন / রেডিমেড লাইব্রেরি
🟢 আপনার সিনট্যাক্স সেট করার জন্য আরও বিকল্প
🔴 বিশেষ সরঞ্জামের ব্যবহার: ANTLR, yacc, lex
🔴 কখনও কখনও ব্যাকরণ বর্ণনা করা কঠিন
🔴 কোন IDE সমর্থন নেই, আপনাকে আপনার প্লাগইন লিখতে হবে
অভ্যন্তরীণ ডিএসএল একটি নির্দিষ্ট সার্বজনীন প্রোগ্রামিং ভাষার (হোস্ট ভাষা) উপর ভিত্তি করে। অর্থাৎ হোস্ট ল্যাঙ্গুয়েজের স্ট্যান্ডার্ড টুলের সাহায্যে লাইব্রেরি তৈরি করা হয় যা আপনাকে আরও কম্প্যাক্টলি লিখতে দেয়। উদাহরণ হিসেবে, ফ্লুয়েন্ট API পদ্ধতি বিবেচনা করুন।
অভ্যন্তরীণ DSL-এর সুবিধা এবং অসুবিধা:
🟢 হোস্ট ভাষার অভিব্যক্তিগুলিকে ভিত্তি হিসাবে ব্যবহার করে
🟢 হোস্ট ভাষার কোডে DSL এম্বেড করা সহজ এবং এর বিপরীতে
🟢 কোড জেনারেশনের প্রয়োজন নেই
🟢 হোস্ট ভাষায় একটি সাবরুটিন হিসাবে ডিবাগ করা যেতে পারে
🔴 সিনট্যাক্স সেট করার ক্ষেত্রে সীমিত সম্ভাবনা
সম্প্রতি, কোম্পানিতে আমরা আমাদের ডিএসএল তৈরি করার প্রয়োজনীয়তার সম্মুখীন হয়েছি। আমাদের পণ্য ক্রয় গ্রহণের কার্যকারিতা বাস্তবায়ন করেছে। এই মডিউলটি BPM (বিজনেস প্রসেস ম্যানেজমেন্ট) এর একটি মিনি-ইঞ্জিন। ব্যবসায়িক প্রক্রিয়াগুলি প্রায়ই গ্রাফিকভাবে উপস্থাপন করা হয়। উদাহরণ স্বরূপ, নিচের BPMN স্বরলিপিটি এমন একটি প্রক্রিয়া দেখায় যেটিতে টাস্ক 1 এবং তারপর টাস্ক 2 এবং টাস্ক 3 সমান্তরালভাবে কার্যকর করা রয়েছে।
গতিশীলভাবে একটি রুট তৈরি করা, অনুমোদনের পর্যায়ের জন্য পারফর্মার সেট করা, স্টেজ এক্সিকিউশনের জন্য সময়সীমা সেট করা ইত্যাদি সহ প্রোগ্রামগতভাবে ব্যবসায়িক প্রক্রিয়া তৈরি করতে সক্ষম হওয়া আমাদের জন্য গুরুত্বপূর্ণ ছিল। এটি করার জন্য, আমরা প্রথমে ফ্লুয়েন্ট API ব্যবহার করে এই সমস্যাটি সমাধান করার চেষ্টা করেছি। পন্থা
তারপরে আমরা উপসংহারে পৌঁছেছি যে ফ্লুয়েন্ট এপিআই ব্যবহার করে গ্রহণযোগ্যতা রুট সেট করা এখনও কষ্টকর হয়ে উঠেছে এবং আমাদের দল তার নিজস্ব ডিএসএল তৈরি করার বিকল্প বিবেচনা করেছে। কোটলিনের উপর ভিত্তি করে একটি বহিরাগত DSL এবং একটি অভ্যন্তরীণ DSL-এ গ্রহণযোগ্যতা রুট কেমন হবে তা আমরা তদন্ত করেছি (কারণ আমাদের পণ্য কোড Java এবং Kotlin এ লেখা আছে)।
বাহ্যিক DSL:
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
এক্সপ্রেশনটি ব্যবহার করে রিসিভার অবজেক্ট অ্যাক্সেস করতে পারেন।
এছাড়াও, যদি মেথড কলের শেষ আর্গুমেন্টটি ল্যাম্বডা হয়, তাহলে ল্যাম্বডা বন্ধনীর বাইরে রাখা যেতে পারে। তাই আমাদের DSL এ আমরা নিচের মত একটি কোড লিখতে পারি:
parallel { addStep { executor = FINANCE_DEPARTMENT ... } addStep { executor = CTO ... } }
এটি সিনট্যাকটিক চিনি ছাড়া একটি কোডের সমতুল্য:
parallel({ this.addStep({ this.executor = FINANCE_DEPARTMENT ... }) this.addStep({ this.executor = CTO ... }) })
রিসিভার সহ Lambdas এবং বন্ধনীর বাইরে Lambda হল Kotlin বৈশিষ্ট্য যা DSL-এর সাথে কাজ করার সময় বিশেষভাবে উপযোগী।
এখন সত্তার 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
আমরা ইনফিক্স ফাংশন 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
কীওয়ার্ড ব্যবহার করে রিসিভার অবজেক্ট অ্যাক্সেস করা হয়।
পরবর্তী Kotlin বৈশিষ্ট্য যা আমরা আমাদের DSL-এ ব্যবহার করি তা হল অপারেটর ওভারলোডিং। আমরা ইতিমধ্যে invoke
অপারেটরের ওভারলোড বিবেচনা করেছি। কোটলিনে, আপনি পাটিগণিত সহ অন্যান্য অপারেটরগুলিকে ওভারলোড করতে পারেন।
addStep { ... +canChange }
এখানে unary অপারেটর +
ওভারলোড হয়। নীচের কোড যা এটি বাস্তবায়ন করে:
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]
উভয় সময়ই বলা হয়েছে। বিশেষ করে এই ধরনের ক্ষেত্রে, Kotlin একটি 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
নীচে এই নিবন্ধে বিবেচিত ভাষার বৈশিষ্ট্যগুলির উপর অফিসিয়াল কোটলিন ডকুমেন্টেশনের লিঙ্কগুলি রয়েছে:
ডোমেন-নির্দিষ্ট ভাষাগুলি একটি নির্দিষ্ট ডোমেনের মধ্যে মডেল এবং সমস্যাগুলি সমাধান করার জন্য একটি বিশেষ এবং অভিব্যক্তিপূর্ণ উপায় প্রদান করে উত্পাদনশীলতা বৃদ্ধি, ত্রুটিগুলি হ্রাস এবং সহযোগিতা উন্নত করার একটি শক্তিশালী উপায় সরবরাহ করে। Kotlin অনেক বৈশিষ্ট্য এবং সিনট্যাকটিক চিনি সহ একটি আধুনিক প্রোগ্রামিং ভাষা, তাই এটি অভ্যন্তরীণ DSL-এর হোস্ট ভাষা হিসাবে দুর্দান্ত।