شعار زيفيرنت

فئات الكتابة في Scala3: دليل المبتدئين | موازنة

التاريخ:

هذه الوثيقة مخصصة لمطور Scala3 المبتدئ الذي هو ضليع بالفعل في نثر Scala، ولكنه في حيرة بشأن كل `implicits`والسمات ذات المعلمات في الكود.

تشرح هذه الوثيقة لماذا وكيف وأين ومتى فئات النوع (TC).

بعد قراءة هذا المستند، سيكتسب مطور Scala3 المبتدئ معرفة قوية لاستخدام الكود المصدري والتعمق فيه كثير من مكتبات Scala وابدأ في كتابة كود Scala الاصطلاحي.

لنبدأ بالسبب…

مشكلة التعبير

في 1998، صرح فيليب وادلر أن "مشكلة التعبير هي اسم جديد لمشكلة قديمة". إنها مشكلة قابلية التوسعة البرمجية. وفقًا لما كتبه السيد وادلر، فإن حل مشكلة التعبير يجب أن يتوافق مع القواعد التالية:

  • القاعدة الأولى: السماح بالتنفيذ السلوكيات الموجودة (فكر في سمة Scala) ليتم تطبيقها عليها تمثيلات جديدة (فكر في فئة الحالة)
  • القاعدة 2:  السماح بتنفيذ سلوكيات جديدة ليتم تطبيقها على التمثيلات الموجودة
  • القاعدة الثالثة: ألا يعرض للخطر اكتب السلامة
  • القاعدة الرابعة: ألا يحتاج إلى إعادة الترجمة الكود الموجود

سيكون حل هذه المشكلة هو الخيط الفضي لهذه المقالة.

القاعدة 1: تنفيذ السلوك الحالي على التمثيل الجديد

تحتوي أي لغة موجهة للكائنات على حل مدمج للقاعدة 1 تعدد الأشكال الفرعي. يمكنك تنفيذ أي `trait`محدد في التبعية على `class` في التعليمات البرمجية الخاصة بك، دون إعادة ترجمة التبعية. دعونا نرى ذلك في العمل:

سكالا

def todo = 42
type Height = Int
type Block = Int

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo


object Lib2:
  import Lib1.*

  case class Polkadot() extends Blockchain:
    override def getBlock(height: Height): Block = todo

val eth = Lib1.Ethereum()
val btc = Lib1.Bitcoin()
val dot = Lib2.Polkadot()

في هذا المثال الوهمي، مكتبة `Lib1`(السطر 5) يحدد السمة `Blockchain`(السطر 6) مع تطبيقين له (السطر 2 و 9). `Lib1` سيظل كما هو في كل هذه الوثيقة (إنفاذ القاعدة 4).

`Lib2`(السطر 15) ينفذ السلوك الموجودBlockchain"في فئة جديدة".Polkadot`(القاعدة 1) بطريقة آمنة من النوع (القاعدة 3)، دون إعادة الترجمة".Lib1"(القاعدة 4). 

القاعدة 2: تنفيذ السلوكيات الجديدة التي سيتم تطبيقها على التمثيلات الموجودة

دعونا نتخيل في `Lib2`نريد سلوكا جديدا`lastBlock` ليتم تنفيذها خصيصًا لكل `Blockchain`.

أول ما يتبادر إلى الذهن هو إنشاء مفتاح كبير بناءً على نوع المعلمة.

سكالا

def todo = 42
type Height = Int
type Block = Int

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo

object Lib2:
  import Lib1.*

  case class Polkadot() extends Blockchain:
    override def getBlock(height: Height): Block = todo

  def lastBlock(blockchain: Blockchain): Block = blockchain match
      case _:Ethereum => todo
      case _:Bitcoin  => todo
      case _:Polkadot => todo
  

object Lib3:
  import Lib1.*

  case class Polygon() extends Blockchain:
    override def getBlock(height: Height): Block = todo

import Lib1.*, Lib2.*, Lib3.*
println(lastBlock(Bitcoin()))
println(lastBlock(Ethereum()))
println(lastBlock(Polkadot()))
println(lastBlock(Polygon()))

هذا الحل هو إعادة تنفيذ ضعيفة لتعدد الأشكال القائم على الكتابة والذي تم دمجه بالفعل في اللغة!

`Lib1` لم يتم المساس بها (تذكر، القاعدة 4 المطبقة في جميع أنحاء هذا المستند). 

تم تنفيذ الحل في `Lib2` هو حسنًا حتى يتم تقديم blockchain آخر في `Lib3`. إنه ينتهك قاعدة أمان النوع (القاعدة 3) لأن هذا الرمز يفشل في وقت التشغيل في السطر 37. وتعديل `Lib2"سوف ينتهك القاعدة 4.

الحل الآخر هو استخدام `extension`.

سكالا

def todo = 42
type Height = Int
type Block = Int

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo

object Lib2:
  import Lib1.*

  case class Polkadot() extends Blockchain:
    override def getBlock(height: Height): Block = todo

    def lastBlock(): Block = todo

  extension (eth: Ethereum) def lastBlock(): Block = todo

  extension (btc: Bitcoin) def lastBlock(): Block = todo

import Lib1.*, Lib2.*
println(Bitcoin().lastBlock())
println(Ethereum().lastBlock())
println(Polkadot().lastBlock())

def polymorphic(blockchain: Blockchain) =
  // blockchain.lastBlock()
  ???

`Lib1` لم يمسها (تطبيق القاعدة 4 في الوثيقة بأكملها). 

`Lib2"يحدد السلوك لنوعه (السطر 21) و"الامتدادات" للأنواع الموجودة (السطران 23 و25)."

الأسطر 28-30، يمكن استخدام السلوك الجديد في كل فصل. 

ولكن لا توجد طريقة لوصف هذا السلوك الجديد بأنه متعدد الأشكال (السطر 32). أي محاولة للقيام بذلك تؤدي إلى أخطاء في الترجمة (السطر 33) أو إلى كتابة مفاتيح تعتمد على الكتابة. 

هذه القاعدة رقم 2 صعبة. لقد حاولنا تنفيذه من خلال تعريفنا الخاص لتعدد الأشكال وخدعة "الامتداد". وكان ذلك غريبا.

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

نمط فئة النوع

تحتوي وصفة نمط Type Class (TC للاختصار) على 3 خطوات. 

  1. تحديد سلوك جديد
  2. تنفيذ السلوك
  3. استخدم السلوك

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

1. تحديد سلوك جديد
سكالا

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

`Lib1`، مرة أخرى، تُركت على حالها.

السلوك الجديد is TC يتجسد في السمة. الوظائف المحددة في السمة هي وسيلة لتطبيق بعض جوانب هذا السلوك.

المعلمة `Aيمثل ` النوع الذي نريد تطبيق السلوك عليه، وهو أنواع فرعية من `Blockchain`في حالتنا.

بعض الملاحظات:

  • إذا لزم الأمر، اكتب المعلمة `A` يمكن تقييده بشكل أكبر بواسطة نظام نوع Scala. على سبيل المثال، يمكننا فرض `A"أن تكون".Blockchain`. 
  • أيضًا، يمكن أن يحتوي TC على العديد من الوظائف المعلنة فيه.
  • وأخيرا، قد يكون لكل وظيفة العديد من المعلمات التعسفية.

ولكن دعونا نجعل الأمور بسيطة من أجل سهولة القراءة.

2. تنفيذ السلوك
سكالا

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  val ethereumLastBlock = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  val bitcoinLastBlock = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

لكل نوع الجديد `LastBlock`السلوك متوقع، هناك مثال محدد لهذا السلوك. 

إن "Ethereum`يتم حساب سطر التنفيذ 22 من `eth` تم تمرير المثيل كمعلمة. 

تنفيذ `LastBlock` لـ `Bitcoinيتم تنفيذ السطر 25 باستخدام إدخال/إخراج غير مُدار ولا يستخدم المعلمة الخاصة به.

لذلك، `Lib2"ينفذ سلوكًا جديدًا".LastBlock` لـ `Lib1`الفصول.

3. استخدم السلوك
سكالا

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  val ethereumLastBlock = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  val bitcoinLastBlock = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A, behavior: LastBlock[A]) =
  behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2), ethereumLastBlock))
println(useLastBlock(Bitcoin(), bitcoinLastBlock))

السطر 30`useLastBlock` يستخدم مثيل `A` و `LastBlock`السلوك المحدد لهذه الحالة.

السطر 33`useLastBlock` يتم استدعاؤه بمثيل `Ethereum` وتنفيذ `LastBlock"محدد في".Lib2`. لاحظ أنه من الممكن تمرير أي تطبيق بديل لـ `LastBlock[A]"(فكر حقن التبعية).

`useLastBlock` هو الغراء بين التمثيل (الفعلي A) وسلوكه. يتم الفصل بين البيانات والسلوك، وهو ما تدعو إليه البرمجة الوظيفية.

مناقشة

دعونا نلخص قواعد مشكلة التعبير:

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

يمكن حل القاعدة 1 خارج الصندوق باستخدام تعدد الأشكال الفرعي.

نمط TC الذي تم تقديمه للتو (انظر لقطة الشاشة السابقة) يحل القاعدة 2. إنه آمن من النوع (القاعدة 3) ولم نلمس أبدًا `Lib1"(القاعدة 4). 

لكن استخدامه غير عملي لعدة أسباب:

  • يتعين علينا في الأسطر 33-34 تمرير السلوك بشكل صريح على طول مثيله. هذا هو النفقات العامة الإضافية. يجب أن نكتب فقط `useLastBlock(Bitcoin())`.
  • السطر 31 بناء الجملة غير شائع. نحن نفضل أن نكتب ملخصًا وأكثر توجهاً للكائنات  `instance.lastBlock()` بيان.

دعونا نسلط الضوء على بعض ميزات Scala للاستخدام العملي لـ TC. 

تجربة مطور محسنة

يتمتع Scala بمجموعة فريدة من الميزات والسكريات النحوية التي تجعل من TC تجربة ممتعة حقًا للمطورين.

ضمنا

النطاق الضمني هو نطاق خاص يتم حله في وقت الترجمة حيث يمكن أن يوجد مثيل واحد فقط من نوع معين. 

يضع البرنامج مثيلًا في النطاق الضمني باستخدام `given`الكلمة الرئيسية. وبدلاً من ذلك، يمكن للبرنامج استرداد مثيل من النطاق الضمني باستخدام الكلمة الأساسية `using`.

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

إنه يختلف عن النطاق العالمي للأسباب التالية: 

  1. لقد تم حلها سياقيا. يمكن لموقعين من البرنامج استخدام مثيل من نفس النوع المحدد في النطاق الضمني، ولكن قد تكون هاتان المثيلتان مختلفتين.
  2. خلف الكواليس، يقوم الكود بتمرير وظيفة الوسائط الضمنية لتعمل حتى يتم الوصول إلى الاستخدام الضمني. إنه لا يستخدم مساحة ذاكرة عالمية.

العودة إلى فئة النوع! لنأخذ نفس المثال بالضبط.

سكالا

def todo = 42
type Height = Int
type Block = Int
def http(uri: String): Block = todo

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo

`Lib1` هو نفس الكود غير المعدل الذي حددناه سابقًا. 

سكالا

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  given ethereumLastBlock:LastBlock[Ethereum] = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given bitcoinLastBlock:LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A)(using behavior: LastBlock[A]) =
  behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2)))
println(useLastBlock(Bitcoin()))

السطر 19 سلوك جديد `LastBlock` تم تعريفه، تمامًا كما فعلنا سابقًا.

السطر 22 والسطر 25، `valيتم استبدال `` بـ ``given`. كلا تطبيقي `LastBlock` يتم وضعها في النطاق الضمني.

السطر 31`useLastBlock`يصرح عن السلوك`LastBlock` كمعلمة ضمنية. يقوم المترجم بحل المثيل المناسب لـ `LastBlock` من النطاق الضمني السياقي من مواقع المتصل (السطران 33 و34). يستورد السطر 28 كل شيء من `Lib2`، بما في ذلك النطاق الضمني. لذلك، يقوم المترجم بتمرير المثيلات المحددة للسطرين 22 و25 كمعلمة أخيرة لـ `useLastBlock`. 

كمستخدم للمكتبة، أصبح استخدام فئة الكتابة أسهل من ذي قبل. السطر 34 و35 يجب على المطور فقط التأكد من إدخال مثيل للسلوك في النطاق الضمني (ويمكن أن يكون هذا مجرد `import`). إذا لم يكن ضمنيًا `given`حيث يوجد الرمز`using"، يقول له المترجم.

يعمل Scala ضمنيًا على تسهيل مهمة تمرير مثيلات الفصل إلى جانب أمثلة سلوكياتهم.

السكريات الضمنية

يمكن تحسين السطر 22 و 25 من الكود السابق بشكل أكبر! دعونا نكرر عمليات تنفيذ TC.

سكالا

given LastBlock[Ethereum] = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

السطران 22 و25، إذا كان اسم المثيل غير مستخدم، فيمكن حذفه.

سكالا


  given LastBlock[Ethereum] with
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] with
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

السطران 22 و 25، يمكن استبدال تكرار النوع بـ `with`الكلمة الرئيسية.

سكالا

given LastBlock[Ethereum] = _.lastBlock

  given LastBlock[Bitcoin] = _ => http("http://bitcoin/last")

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

يقدم Scala سكريات نحوية لتبسيط بناء الجملة، وإزالة التسمية والإعلان والتكرار غير الضروري.

تمديد

تستخدم بحكمة، `extensionيمكن للآلية تبسيط بناء الجملة لاستخدام فئة الكتابة.

سكالا

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  given LastBlock[Ethereum] with
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] with
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

  extension[A](instance: A)
    def lastBlock(using tc: LastBlock[A]) = tc.lastBlock(instance)

import Lib1.*, Lib2.*

println(Ethereum(lastBlock = 2).lastBlock)
println(Bitcoin().lastBlock)

الأسطر 28-29 طريقة تمديد عامة `lastBlock` يتم تعريفه لأي `A`مع`LastBlock`معلمة TC في النطاق الضمني.

الأسطر 33-34 يعمل الامتداد على تعزيز بناء الجملة الموجه للكائنات لاستخدام TC.

سكالا

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  given LastBlock[Ethereum] with
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] with
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

  extension[A](instance: A)(using tc: LastBlock[A])
    def lastBlock = tc.lastBlock(instance)
    def penultimateBlock = tc.lastBlock(instance) - 1

import Lib1.*, Lib2.*

val eth = Ethereum(lastBlock = 2)
println(eth.lastBlock)
println(eth.penultimateBlock)

val btc = Bitcoin()
println(btc.lastBlock)
println(btc.penultimateBlock)

في السطر 28، يمكن أيضًا تحديد معلمة TC للامتداد بالكامل لتجنب التكرار. السطر 30 نعيد استخدام TC في الامتداد لتعريف `penultimateBlock`(على الرغم من إمكانية تنفيذه على `LastBlock` سمة مباشرة)

يحدث السحر عند استخدام TC. يبدو التعبير أكثر طبيعية، مما يعطي الوهم بأن السلوك `lastBlock` يتم الخلط مع المثيل.

نوع عام مع TC
سكالا

import Lib1.*, Lib2.*

def useLastBlock1[A](instance: A)(using LastBlock[A]) = instance.lastBlock

def useLastBlock2[A: LastBlock](instance: A) = instance.lastBlock

val eth = Ethereum(lastBlock = 2)
assert(useLastBlock1(eth) == useLastBlock2(eth))

السطر 34 تستخدم الوظيفة TC ضمنيًا. لاحظ أنه لا يلزم تسمية TC إذا كان هذا الاسم غير ضروري.

يتم استخدام نمط TC على نطاق واسع بحيث يوجد بناء جملة نوع عام للتعبير عن "نوع ذو سلوك ضمني". السطر 36 بناء الجملة هو بديل أكثر إيجازا للسطر السابق (السطر 34). إنه يتجنب الإعلان على وجه التحديد عن معلمة TC الضمنية غير المسماة.

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

الاشتقاق التلقائي

تستخدم الكثير من مكتبات Scala TC، وتترك للمبرمج تنفيذها في قاعدة التعليمات البرمجية الخاصة بها.

على سبيل المثال، تستخدم Circe (مكتبة إلغاء تسلسل json) TC `Encoder[T]"و"Decoder[T]` ليتمكن المبرمجون من تنفيذها في قاعدة التعليمات البرمجية الخاصة بهم. وبمجرد تنفيذه، يمكن استخدام النطاق الكامل للمكتبة. 

يتم تنفيذ تطبيقات TC هذه في كثير من الأحيان مصممو الخرائط الموجهة نحو البيانات. لا يحتاجون إلى أي منطق عمل، كما أن كتابتهم مملة، ويشكل الحفاظ على التزامن مع فئات الحالة عبئًا.

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

تحت غطاء محرك السيارة، في وقت الترجمة، استبطان وحدات الماكرو العامة أنواع كبنية بيانات خالصة وإنشاء TC[T] لمستخدمي المكتبة. 

يعد اشتقاق TC بشكل عام أمرًا شائعًا جدًا، لذلك قدم Scala صندوق أدوات كاملًا لهذا الغرض. لا يتم دائمًا الإعلان عن هذه الطريقة من خلال وثائق المكتبة على الرغم من أنها طريقة Scala 3 لاستخدام الاشتقاق.

سكالا

object GenericLib:

  trait Named[A]:
    def blockchainName(instance: A): String

  object Named:
    import scala.deriving.*

    inline final def derived[A](using inline m: Mirror.Of[A]): Named[A] =
      val nameOfType: String = inline m match
        case p: Mirror.ProductOf[A] => compiletime.constValue[p.MirroredLabel]
        case _ => compiletime.error("Not a product")
      new Named[A]:
        override def blockchainName(instance: A):String = nameOfType.toLowerCase

  extension[A] (instance: A)(using tc: Named[A])
    def blockchainName = tc.blockchainName(instance)

import Lib1.*, GenericLib.*

case class Polkadot() derives Named
given Named[Bitcoin] = Named.derived
given Named[Ethereum] = Named.derived

println(Ethereum(lastBlock = 2).blockchainName)
println(Bitcoin().blockchainName)
println(Polkadot().blockchainName)

السطر 18 TC جديد `Named"تم تقديمه. لا يرتبط TC هذا بأعمال blockchain بالمعنى الدقيق للكلمة. والغرض منه هو تسمية blockchain بناءً على اسم فئة الحالة.

ركز أولاً على سطور التعاريف 36-38. هناك صيغتان لاشتقاق TC:

  1. السطر 36 يمكن تعريف مثيل TC مباشرة في فئة الحالة باستخدام `derives`الكلمة الرئيسية. تحت الغطاء، يقوم المترجم بإنشاء ملف `Named`مثال في`Polkadot"كائن مصاحب.
  2. السطر 37 و38، يتم تقديم مثيلات فئات النوع في الفئات الموجودة مسبقًا باستخدام `TC.derived

السطر 31 تم تعريف الامتداد العام (انظر الأقسام السابقة) و`blockchainName` يستخدم بشكل طبيعي.  

إن "derives` الكلمة الأساسية تتوقع طريقة بالنموذج `inline def derived[T](using Mirror.Of[T]): TC[T] = ???"الذي تم تعريفه في السطر 24. لن أشرح بالتفصيل ما يفعله الكود. في الخطوط العريضة:

  • `inline def`يحدد الماكرو
  • `Mirror` هو جزء من صندوق الأدوات لاستبطان الأنواع. هناك أنواع مختلفة من المرايا، ويركز الكود في السطر 26 على `Product`المرايا (فئة الحالة هي منتج). السطر 27، إذا حاول المبرمجون استخلاص شيء ليس `Product`، لن يتم تجميع الكود.
  • ال "Mirror`يحتوي على أنواع أخرى. واحد منهم "MirrorLabel`، هي سلسلة تحتوي على اسم النوع. يتم استخدام هذه القيمة في تنفيذ السطر 29 من `Named` ح.

يمكن لمؤلفي TC استخدام البرمجة التعريفية لتوفير الوظائف التي تولد بشكل عام مثيلات TC بنوع محدد. يمكن للمبرمجين استخدام واجهة برمجة تطبيقات المكتبة المخصصة أو أدوات اشتقاق Scala لإنشاء مثيلات للتعليمات البرمجية الخاصة بهم.

سواء كنت بحاجة إلى تعليمات برمجية عامة أو محددة لتنفيذ TC، يوجد حل لكل موقف. 

ملخص لجميع الفوائد

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

مؤشرات مضادة

تم تصميم كل مطرقة لمجموعة من المشاكل.

فئات النوع مخصصة للمشكلات السلوكية ويجب عدم استخدامها لوراثة البيانات. استخدم التركيبة لهذا الغرض.

النوع الفرعي المعتاد هو أكثر وضوحا. إذا كنت تمتلك قاعدة التعليمات البرمجية ولا تهدف إلى التوسعة، فقد تكون فئات الكتابة مبالغة.

على سبيل المثال، في Scala core، يوجد `Numeric`نوع الفئة:

سكالا

trait Numeric[T] extends Ordering[T] {
  def plus(x: T, y: T): T
  def minus(x: T, y: T): T
  def times(x: T, y: T): T

من المنطقي حقًا استخدام فئة النوع هذه لأنها لا تسمح فقط بإعادة استخدام الخوارزميات الجبرية على الأنواع المضمنة في Scala (Int، ​​BigInt، ...)، ولكن أيضًا على الأنواع المحددة من قبل المستخدم (a `ComplexNumber`على سبيل المثال).

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

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

وفي الختام

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

آمل أن تكون قد اكتسبت المعرفة بقراءة هذه الوثيقة. 

الكود متاح في https://github.com/jprudent/type-class-article. يرجى التواصل معي إذا كان لديك أي نوع من الأسئلة أو الملاحظات. يمكنك استخدام المشكلات أو تعليقات التعليمات البرمجية في المستودع إذا كنت تريد ذلك.


جيروم برودنت

مهندس البرمجيات

بقعة_صورة

أحدث المعلومات الاستخباراتية

بقعة_صورة