5 خطوات لإنشاء أول فئة Class في Scala

في منشور المدونة هذا ، ستتعرف على كيفية تطبيق الفصل الدراسي الأول ، وهو ميزة لغة أساسية في أيقونة لغات البرمجة الوظيفية - Haskell.

الصورة ستانلي داي على Unsplash

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

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

إنشاء الدرجة الأولى الخاصة بك

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

كمثال في هذه المقالة ، سأستخدم مكتبة النوع Eq من Cats.

سمة Eq [A] {
  def areEquals (a: A، b: A): Boolean
}

النوع Class Eq [A] عبارة عن عقد له القدرة على التحقق مما إذا كان هناك نوعان من الكائنات من النوع "أ" متساويان بناءً على بعض المعايير المطبقة في طريقة "المساواة".

إن إنشاء مثيل لفئة النوع لدينا بسيط مثل فئة إنشاء مثيل تمد السمة المذكورة بفارق واحد فقط بحيث يمكن الوصول إلى مثيل فئة النوع لدينا ككائن ضمني.

def moduloEq (divisor: Int): Eq [Int] = new Eq [Int] {
 over defide def areEquals (a: Int، b: Int) =٪ divisor == b٪ divisor
}
الضمنية val modulo5Eq: Eq [Int] = moduloEq (5)

يمكن ضغط الجزء العلوي من الشفرة قليلاً في النموذج التالي.

def moduloEq: Eq [Int] = (a: Int، b: Int) => a 5٪ == b٪ 5

لكن مهلا ، كيف يمكنك تعيين دالة (Int ، Int) => منطقية للإشارة مع النوع Eq [Int] ؟! هذا الشيء ممكن بفضل خاصية Java 8 التي تسمى Single Abstract Method نوع الواجهة. يمكننا أن نفعل مثل هذا الشيء عندما يكون لدينا طريقة واحدة مجردة فقط في سمة لدينا.

اكتب القرار الدرجة

في هذه الفقرة ، سأوضح لك كيفية استخدام مثيلات فئة الكتابة وكيفية الربط السحري معًا معًا كتابة الفئة Eq [A] بالكائن المقابل من النوع A عندما يكون ذلك ضروريًا.

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

def pairEquals [A] (a: A، b: A) (eq الضمني: Eq [A]): ​​الخيار [(A، A)] = {
 إذا (eq.areEquals (a، b)) بعض ((a، b)) لا شيء آخر
}

لقد قمنا بمزاوجة دالة الزوجية للعمل مع أي أنواع توفر مثيلاً للفئة Eq [A] المتاحة في نطاقها الضمني.

عندما يتعذر على برنامج التحويل البرمجي العثور على أي مثيل يطابق الإعلان أعلاه ، سينتهي الأمر بتحذير خطأ في الترجمة حول عدم وجود مثيل مناسب في النطاق الضمني المقدم.
  1. سيقوم المترجم باستنتاج نوع المعلمات المقدمة من خلال تطبيق الوسائط على وظيفتنا وتعيينها إلى الاسم المستعار A.
  2. الوسيطة السابقة eq: Eq [A] مع الكلمة الأساسية ضمنيًا ستطلق اقتراحًا للبحث عن كائن من النوع Eq [A] في النطاق الضمني.

بفضل المتضمنات والمعلمات المكتوبة ، يستطيع برنامج التحويل البرمجي ربط الفئة مع مثيل فئة النوع المقابل لها.

يتم تحديد جميع الحالات والوظائف ، دعونا نتحقق مما إذا كان الرمز الخاص بنا يعطي نتائج صالحة

pairEquals (2،7)
res0: الخيار [(Int، ​​Int)] = بعض ((2،7))
pairEquals (2،3)
res0: الخيار [(Int، ​​Int)] = بلا

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

حدود السياق

أول شيء أريد تحسينه في التعليمات البرمجية الخاصة بنا هو التخلص من قائمة الوسائط الثانية (التي تتضمن كلمة أساسية ضمنية). نحن لا نمرر تلك الوظيفة مباشرة عند استدعاء الوظيفة ، لذلك دعونا نضمن ضمنيًا مرة أخرى. في Scala ، يمكن استبدال الوسائط الضمنية ذات معلمات الكتابة بإنشاء لغة تسمى Context Bound.

Context Bound عبارة عن تعريف في قائمة معلمات النوع والتي بناء الجملة A: Eq تقول أن كل نوع يستخدم كوسيطة للدالة pairEquals يجب أن يكون له قيمة ضمنية من النوع Eq [A] في النطاق الضمني.

def pairEquals [A: Eq] (a: A، b: A): Option [(A، A)] = {
 إذا (ضمنيًا [Eq [A]]. areEquals (a، b)) Some ((a، b)) else None
}

كما لاحظت ، انتهى بنا المطاف دون الإشارة إلى القيمة الضمنية. للتغلب على هذه المشكلة ، نستخدم الدالة ضمنيًا [F [_]] والتي تسحب القيمة الضمنية الموجودة عن طريق تحديد النوع الذي نشير إليه.

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

ما يمكننا القيام به هو توفير وظيفة تطبيق معلمة في كائن مصاحب لفئة النوع لدينا.

كائن مكافئ {
 تطبيق def [A] (مكافئ ضمني: Eq [A]): ​​Eq [A] = مكافئ
}

هذا الشيء البسيط حقًا يسمح لنا بالتخلص من ضمنيًا وسحب مثيلنا من طي النسيان ليتم استخدامه في منطق النطاق دون النمطي.

def pairEquals [A: Eq] (a: A، b: A): Option [(A، A)] = {
 إذا (Eq [A] .areEquals (a، b)) Some ((a، b)) else None
}

التحويلات الضمنية - الملقب. بناء الجملة وحدة

الشيء التالي الذي أرغب في الحصول عليه على طاولة العمل هو Eq [A] .areEquals (a، b). يبدو بناء الجملة هذا مطولًا جدًا لأننا نشير صراحة إلى نوع فئة المثال الذي يجب أن يكون ضمنيًا ، أليس كذلك؟ الشيء الثاني هو أن مثيل فئة النوع هنا يعمل مثل الخدمة (بمعنى DDD) بدلاً من امتداد الفئة الحقيقي. لحسن الحظ ، يمكن أيضًا إصلاحها بمساعدة استخدام مفيد آخر للكلمة الرئيسية الضمنية.

ما سنفعله هنا هو توفير ما يسمى بناء الجملة أو (العمليات كما هو الحال في بعض مكتبات FP) باستخدام التحويلات الضمنية التي تسمح لنا بتوسيع API لبعض الفئات دون تعديل الكود المصدر.

الفئة الضمنية EqSyntax [A: Eq] (a: A) {
 def === (b: A): Boolean = Eq [A] .areEquals (a، b)
}

يخبر هذا الرمز المحول البرمجي بتحويل الفئة (أ) وجود مثيل لنوع الفئة Eq [A] إلى الفئة EqSyntax التي لها دالة واحدة ===. تجعل كل هذه الأشياء انطباعًا بأننا أضفنا وظيفة === إلى الفئة "أ" بدون تعديل شفرة المصدر.

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

الآن يُسمح لنا بتطبيق الطريقة === على الكتابة "أ" عندما يكون لدينا فئة EqSyntax في النطاق. الآن سوف يتغير تطبيقنا لـ pairEquals قليلاً ، وسيكون كما يلي.

def pairEquals [A: Eq] (a: A، b: A): Option [(A، A)] = {
 إذا (أ === ب) بعض ((أ ، ب)) لا شيء آخر
}

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

نطاق ضمني

كما ترى ، فإن فئات الكتابة في Scala تعتمد اعتمادًا صارمًا على استخدام ميزة ضمنية ، لذلك من الضروري فهم كيفية التعامل مع النطاق الضمني.

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

1. الحالات المحلية والموروثة
2. الحالات المستوردة
3. تعريفات من الكائن المرافق للفئة type أو المعلمات

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

لذلك دعونا نناقش النقطة 3 باستخدام مثال للوظيفة المعروفة من مكتبة Scala القياسية التي صنفت الوظيفة التي تستند إلى المقارنة المقدمة ضمنيًا.

مرتبة [B>: A] (ord ضمني: math.Ordering [B]): List [A]

سيتم البحث عن مثيل فئة الفئة في:
 * ترتيب كائن رفيق
 * قائمة كائن رفيق
 كائن B رفيق (والذي يمكن أن يكون أيضًا كائن مصاحب بسبب وجود تعريف حدود منخفضة)

الزائفة

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

التغيير الوحيد الذي يجب أن نقدمه هو التعليق التوضيحيtypeclass الذي يعد علامة لوحدات الماكرو لتوسيع وحدة بناء الجملة الخاصة بنا.

استيراد simulacrum.
typeclass trait Eq [A] {
 op ("===") def areEquals (a: A، b: A): Boolean
}

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

شكرًا لك على القراءة ، سأقدر حقًا أي تعليقات منك ، وإنني أتطلع للقاء معك في المستقبل بمقال منشور آخر.