المُنشِئ الحافظ للنوع (Type-Safe Builder) في لغة Kotlin
المُنشِئ الحافظ للنوع (Type-Safe Builder)
يُتاح بناءُ المُنشِئ الستاتيكيّ الحافظ للنوع في لغة Kotlin باستخدام الدوال ذات التسمية المعبِّرة كمُنشِئ (builder) بالإضافة إلى قيم حرفية (literals) للدوال مع المستقبِل (receiver)، إذ يسمح المُنشِئ الحافظ للنوع ببناء لغات مُخصَّصة المجال (DSL) بالاعتماد على Kotlin بما يتناسب مع إنشاء بُنى البيانات الهرميّة المعقَّدة بطريقةٍ نصف تصريحية (semi-declarative)، وهذه بعض الأمثلة من حالات استخدامه:
- توليد ترميزٍ (markup) باستخدام شيفرة Kotlin مثل HTML أو XML
- تصميم أجزاء واجهات المستخدم (UI) برمجيًا مثل Anko
- ضبط مسارات (routes) خادم الويب مثل Ktor
مثال عن المُنشِئ الحافظ للنوع
لتكن الشيفرة الآتية:
import com.example.html.* // راجع التصريحات بالأسفل
fun result(args: Array<String>) =
html {
head {
title {+"XML encoding with Kotlin"}
}
body {
h1 {+"XML encoding with Kotlin"}
p {+"this format can be used as an alternative markup to XML"}
// عنصر يحتوي على خواصّ ومحتوى نصّيّ
a(href = "http://kotlinlang.org") {+"Kotlin"}
// محتوى مختلط
p {
+"This is some"
b {+"mixed"}
+"text. For more see the"
a(href = "http://kotlinlang.org") {+"Kotlin"}
+"project"
}
p {+"some text"}
// محتوى مُولَّد
p {
for (arg in args)
+arg
}
}
}
وهي شيفرة تخضع تمامًا لقواعد لغة Kotlin، وبإمكانك تعديل الشيفرة وتشغيلها في المستعرض.
آلية العمل
يجب أولًا تعريف النموذج المطلوب بناؤه؛ وهو نموذج وسوم HTML (HTML tags) في مثالنا، ويُستخدَم لذلك عددٌ من الأصناف (classes)، فتكون مثلًا الكلمة المفتاحيّة HTML
صنفًا يعبِّر عن العنصر <html>
، وهو يعرِّف كافّة العناصر الأبناء له مثل <head>
و <body>
، يكون الشكل العام للشيفرة:
html {
// ...
}
إذ إنّ html
هو استدعاء دالةٍ تقبل تعبير lambda كوسيط (argument)، وتُعرَّف هذه الدالة بالشكل:
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
يُلاحظ أنّ لهذه الدالة معاملٌ واحدٌ باسم init
والذي هو بحدّ ذاته دالة، أمّا نوع الدالة فهو HTML.() -> Unit
أي هو نوع دالةٍ بمستقبِل (receiver)، ولذلك يجب تمرير كائنٍ من النوع HTML
(كمستقبِلٍ) للدالة حيث يُسمَح باستدعاء عناصر هذا الكائن داخل الدالة، وتُستخدَم الكلمة المفتاحية this
للوصول إلى المستقبِل كما يلي:
html {
this.head { /* ... */ }
this.body { /* ... */ }
}
حيث إن head
و body
هما دالتان للصنف HTML
، وعند الاستغناء عن this
-وهذا ممكن- ستنتج شيفرةٌ مشابهةٌ جدًا لبنية المنشِئ (builder) بالشكل:
html {
head { /* ... */ }
body { /* ... */ }
}
لو نظرنا إلى بُنية الدالة html
المُعرَّفة سابقًا فسنجد أنّها تُنشِئ كائنًا من HTML
وتهيّئُه عبر استدعاء الدالة المُمرَّرة كمعاملٍ (وهي في مثالنا إما head
أو body
عبر الكائن من HTML
) ثم تعيد ذلك الكائن، وهذا بالضبط ما يجب أن يقوم به المُنشِئ.
وتُعرَّف الدالتان head
و body
في الصنف HTML
بشكلٍ مشابهٍ لتعريف html
، وتختلفان عنها بأمرٍ واحدٍ فقط من حيث أنهما تضيفان الكائن المنشَأ إلى مجموعة children
للكائن من HTML
المحيط (enclosing) بهما، كما في الشيفرة:
fun head(init: Head.() -> Unit) : Head {
val head = Head()
head.init()
children.add(head)
return head
}
fun body(init: Body.() -> Unit) : Body {
val body = Body()
body.init()
children.add(body)
return body
}
وتقوم الدالتان -في الواقع- بنفس المهمة، لذلك يُستفاد من تعميم initTag
بالشكل:
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
وبهذا تصبح الدالتان بشكلٍ أبسط كما يلي:
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
ويمكن استخدامهما لبناء العنصرين <head>
و <body>
، أمّا لإضافة نصّ إلى جسم الوسم (tag body) تصبح الشيفرة كما يلي:
html {
head {
title {+"XML encoding with Kotlin"}
}
// ...
}
حيث أُضيفت السلسلة النصّيّة المطلوبة مسبوقةً بالمعامل الأحادي (unary operator) +
، والذي سيقوم بالعملية الأحاديّة البادئة (prefix) المُعرَّفة عبر الدالة الإضافيّة unaryPlus()
والموجودة في الصنف المُجرَّد TagWithText
الواقع بمستوى أعلى من Title
(أبٌ له) بالشكل:
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
إذ تتلخص مهمّة المعامل +
هنا بتغليف (wrap) السلسلة في كائنٍ من TextElement
وإضافته إلى مجموعة children
بجيث يصبح جزءًا صحيحًا من شجرة الوسوم (tag tree).
ويكون كل ماسبق مُعرَّفًا في الحزمة com.example.html
التي يجري استيرادها في بداية شيفرة المُنشِئ (بالأعلى)، ويتوفَّر التعريف الكامل للحزمة في الفقرة الأخيرة من هذه الصفحة.
التحكُّم بالمجال (Scope) عبر @DslMarker
(بدءًا من الإصدار 1.1)
عند استخدام لغات البرمجة مُخصَّصة المجال (DSL) ستظهر مشكلة استدعاء الكثير من الدوال في سياق البرنامج حيث بالإمكان استدعاء التوابع (methods) من كل مستقبلٍ ضمنيٍّ مُمكنٍ داخل lambda، وقد تصدر عن هذا نتيجةٌ متناقضةٌ كأن يكون الوسم head
داخل وسم head
آخر مثل:
html {
head {
head {} // يجب منع حدوث هذا
}
// ...
}
إذ يجب السماح بعناصر المستقبِل (receiver) الضمنيّ this@head
الأقرب فقط، ومنع استدعاء head()
لأنها عنصرٌ من مستقبِلٍ خارجيٍّ this@html
.
ولحلِّ هذه المشكلة طُرحت آليةٌ خاصةٌ بدءًا من الإصدار Kotlin 1.1 بهدف التحكُّم بمجال المستقبِل (receiver scope)، وذلك من خلال توصيف (annotate) أنواع كلِّ المستقبِلات المُستخدَمة في DSL بنفس التوصيف المُحدِّد (marker annotation)، فيُستخدَم مثلًا التوصيف @HTMLTagMarker
لمُنشِئ HTML
بالشكل:
@DslMarker
annotation class HtmlTagMarker
ويدعى صنف التوصيف باسم DSL marker (مُحدِّد DSL) إن كان له التوصيف @DslMarker
، وتُعدُّ كلُّ أصناف الوسوم في مثالنا توسعةً (extend) عن الصنف الأعلى (superclass) ذاته Tag
، ولذلك يكفي توصيف الصنف الأعلى بالتوصيف @HtmlTagMarker
، وسيعامِل المُترجِم حينها كافّةً الأصناف الموروثة (inherited) وكأنها مُوصَّفة، كما يلي:
@HtmlTagMarker
abstract class Tag(val name: String) { ... }
ولا يجب إذن توصيف الأصناف HTML
أو Head
بالتوصيف @HtmlTagMarker
بسبب احتواء صنفهم الأعلى (superclass) على التوصيف:
class HTML() : Tag("html") { ... }
class Head() : Tag("head") { ... }
وبعد القيام بتلك الخطوة، سيحدِّد المُترجِم أيّ المستقبِلات الضمنيّة جزءٌ من نفس DSL ليسمح باستدعاء العناصر من المستقبِلات الأقرب فقط، مثل:
html {
head {
head { } // خطأ: عنصر من مستقبل خارجي
error: a member of outer receiver
}
// ...
}
ويبقى بالإمكان استدعاء عناصر من المستقبلات الخارجيّة ولكن يجب حينئذٍ تحديدُ المستقبِل بشكلٍ صريحٍ (explicit) كما يلي:
html {
head {
this@html.head { } // شيفرة صحيحة
}
// ...
}
التعريف الكامل للحزمة com.example.html
توضِّح الشيفرة الآتية تعريف الحزمة com.example.html
لأجل العناصر المذكورة مسبقًا وحسب، إذ تبني هذه الحزمة شجرة HTML، وترتكز بشكل مكثَّفٍ على استخدام الدوال الإضافيّة (extension functions) وتعابير lambda بالمستقبِلات.
تذكّر أنّ التوصيف @DslMarker
متاحٌ فقط بدءًا من الإصدار Kotlin 1.1.
package com.example.html
interface Element {
fun render(builder: StringBuilder, indent: String)
}
class TextElement(val text: String) : Element {
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent$text\n")
}
}
@DslMarker
annotation class HtmlTagMarker
@HtmlTagMarker
abstract class Tag(val name: String) : Element {
val children = arrayListOf<Element>()
val attributes = hashMapOf<String, String>()
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent<$name${renderAttributes()}>\n")
for (c in children) {
c.render(builder, indent + " ")
}
builder.append("$indent</$name>\n")
}
private fun renderAttributes(): String {
val builder = StringBuilder()
for ((attr, value) in attributes) {
builder.append(" $attr=\"$value\"")
}
return builder.toString()
}
override fun toString(): String {
val builder = StringBuilder()
render(builder, "")
return builder.toString()
}
}
abstract class TagWithText(name: String) : Tag(name) {
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
}
class HTML : TagWithText("html") {
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
class Head : TagWithText("head") {
fun title(init: Title.() -> Unit) = initTag(Title(), init)
}
class Title : TagWithText("title")
abstract class BodyTag(name: String) : TagWithText(name) {
fun b(init: B.() -> Unit) = initTag(B(), init)
fun p(init: P.() -> Unit) = initTag(P(), init)
fun h1(init: H1.() -> Unit) = initTag(H1(), init)
fun a(href: String, init: A.() -> Unit) {
val a = initTag(A(), init)
a.href = href
}
}
class Body : BodyTag("body")
class B : BodyTag("b")
class P : BodyTag("p")
class H1 : BodyTag("h1")
class A : BodyTag("a") {
var href: String
get() = attributes["href"]!!
set(value) {
attributes["href"] = value
}
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}