هل استطيع هاز؟ ضرب من قبل البرمجة النوع العام

مرحبا يا هبر.


في المرة الأخيرة ، وصفنا " Has نمط" ، أوجزنا المشكلات التي حلها ، وكتبنا بعض الحالات المحددة:


 instance HasDbConfig AppConfig where getDbConfig = dbConfig instance HasWebServerConfig AppConfig where getWebServerConfig = webServerConfig instance HasCronConfig AppConfig where getCronConfig = cronConfig 

انها تبدو جيدة. ما الصعوبات التي يمكن أن تنشأ هنا؟


صورة


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


 instance HasDbConfig DbConfig where getDbConfig = id 

أنها تتيح لنا بسهولة كتابة الاختبارات الفردية أو الأدوات المساعدة المساعد مستقلة عن AppConfig بأكمله.


هذا ممل بالفعل ، لكنه لا يزال مستمراً. من السهل أن نتخيل أن بعض اختبارات التكامل تتحقق من تفاعل زوج من الوحدات ، وما زلنا لا نريد الاعتماد على تكوين التطبيق بأكمله ، لذلك نحن بحاجة الآن إلى كتابة ست حالات (حالتان لكل نوع) ، سيتم تخفيض كل واحدة منها إلى fst أو snd . على سبيل المثال ، من أجل DbConfig :


 instance HasDbConfig (DbConfig, b) where getDbConfig = fst instance HasDbConfig (a, DbConfig) where getDbConfig = snd 

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


إذا كنت مهتمًا بكيفية حل هذه المشكلة بشكل عام ، علاوة على ذلك ، فهذه أنواع تابعة ، وكيف سينتهي كل الأمر وكأنها قطة Haskell - Welkom.


تلخيص الفصل


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


 class Has part record where extract :: record -> part 

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


 doSmthWithDbAndCron :: (MonadReader rm, HasDbConfig r, HasCronConfig r) => ... 

في


 doSmthWithDbAndCron :: (MonadReader rm, Has DbConfig r, Has CronConfig r) => ... 

التغيير الوحيد هو بضع مسافات في الأماكن الصحيحة.


بالإضافة إلى ذلك ، لم نفقد الكثير في استنتاج الكتابة: لا يزال بإمكان المؤقت إخراج قيمة الإرجاع المطلوبة من extract في السياق المحيط في الغالبية العظمى من الحالات التي صودفت في الممارسة العملية.


الآن بما أننا لا نهتم بنوع البيئة المحدد ، فلنرى السجلات التي يمكنها تنفيذ فئة Has part record الخاص part الثابت. هذه المهمة لديها هيكل حثي جيد:


  1. كل نوع له نفسه: يتم تنفيذ Has record record بطريقة تافهة ( extract = id ).
  2. إذا كان record عبارة عن منتج لأنواع rec1 و rec2 ، فسيتم تنفيذ Has part record إذا كان فقط إذا كان Has part rec1 أو Has part rec2 .
  3. إذا كان record هو مجموع الأنواع rec1 و rec2 ، فسيتم تنفيذ Has part record إذا وفقط إذا كان Has part rec1 و Has part rec2 . على الرغم من أن الانتشار العملي لهذه الحالة في هذا السياق ليس واضحًا ، إلا أنه لا يزال من الجدير بالذكر اكتماله.

لذلك ، يبدو أننا قمنا بصياغة مخطط لخوارزمية لتحديد ما إذا كان Has part record تم تطبيق record part على part record البيانات تلقائيًا!


لحسن الحظ ، فإن مثل هذا التفكير الاستقرائي للأنواع يناسب تمامًا آلية Haskell Generics . باختصار وتبسيط ، يعد Generics أحد أساليب البرمجة الوصفية المعممة في Haskell ، وينتج عن الملاحظة أن كل نوع إما عبارة عن نوع sum أو نوع منتج أو نوع أساسي وحيد البنية مع حقل واحد.


لن أكتب برنامجًا تعليميًا آخر عن الأدوية الجنيسة ، لذا انتقل إلى الرمز.


المحاولة الأولى


سوف نستخدم الطريقة الكلاسيكية للتطبيق Generic لـ Has خلال فئة GHas المساعدة:


 class GHas part grecord where gextract :: grecord p -> part 

هنا grecord هو تمثيل عام لنوع record بنا.


تتبع تطبيقات GHas التركيب الاستقرائي الذي GHas أعلاه:


 instance GHas record (K1 i record) where gextract (K1 x) = x instance GHas part record => GHas part (M1 it record) where gextract (M1 x) = gextract x instance GHas part l => GHas part (l :*: r) where gextract (l :*: _) = gextract l instance GHas part r => GHas part (l :*: r) where gextract (_ :*: r) = gextract r 

  1. K1 يتوافق مع القضية الأساسية.
  2. M1 - البيانات الوصفية الخاصة بالأدوية التي لا نحتاج إليها في مهمتنا ، لذلك نحن ببساطة نتجاهلها ونتعرف عليها.
  3. المثيل الأول لنوع المنتج l :*: r يتوافق مع الحالة عندما يكون للجزء "الأيسر" من المنتج قيمة part النوع الذي نحتاجه (ربما ، بشكل متكرر).
  4. وبالمثل ، فإن المثيل الثاني لنوع المنتج l :*: r يتوافق مع الحالة التي يكون فيها للجزء "الأيمن" من المنتج قيمة part الكتابة الذي نحتاج إليه (بشكل طبيعي ، أيضًا ، بشكل متكرر).

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


بالإضافة إلى ذلك ، من المفيد أن نلاحظ أن كل منها نيمكن تمثيل نوع المنتج -ary (a1, ..., an) على أنه تكوين ن1دولاأزواج (a1, (a2, (a3, (..., an)))) ، لذلك اسمحوا لي أن أربط أنواع المنتجات مع أزواج.


من خلال GHas لدينا ، يمكنك كتابة تطبيق افتراضي لـ Has الذي يستخدم الأدوية العامة:


 class Has part record where extract :: record -> part default extract :: Generic record => record -> part extract = gextract . from 

القيام به.


أم لا؟


المشكلة


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


قد يكون لدينا


 instance context => Foo barPattern bazPattern where ... 

(بالمناسبة ، يسمى هذا الشيء بعد => رأس المثيل.)


يبدو من الطبيعي قراءة هذا كـ


دعونا نحتاج إلى اختيار مثيل لـ Foo bar baz . إذا كان context مقتنعًا ، فيمكنك تحديد هذا المثال شريطة أن يتوافق bar baz مع barPattern و bazPattern .

ومع ذلك ، هذا تفسير خاطئ ، والعكس صحيح:


دعونا نحتاج إلى اختيار مثيل لـ Foo bar baz . إذا كان bar baz و baz barPattern مع barPattern و bazPattern ، فعندئذ نختار هذا المثيل ونضيف context إلى قائمة الثوابت التي يجب حلها.

الآن أصبح من الواضح ما المشكلة. دعنا نلقي نظرة فاحصة على الحالات التالية:


 instance GHas part l => GHas part (l :*: r) where gextract (l :*: _) = gextract l instance GHas part r => GHas part (l :*: r) where gextract (_ :*: r) = gextract r 

لديهم نفس رؤساء المثال ، لذلك لا عجب أنها تتقاطع! بالإضافة إلى ذلك ، لا يوجد منهم أكثر تحديدًا من الآخر.


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


أنواع معبرة الاندفاع إلى الإنقاذ!


يكمن حل المشكلة في حساب "المسار" مسبقًا على القيمة التي تهمنا ، واستخدام هذا المسار لتوجيه اختيار الحالات.


نظرًا لأننا وافقنا على عدم دعم أنواع المبلغ ، فإن المسار بالمعنى الحرفي هو سلسلة من المنعطفات اليسرى أو اليمنى في أنواع المنتجات (أي اختيارات المكون الأول أو الثاني من الزوج) ، تنتهي بمؤشر كبير "هنا" ، بمجرد العثور على النوع المرغوب . نكتب هذا:


 data Path = L Path | R Path | Here deriving (Show) 

على سبيل المثال

النظر في الأنواع التالية:


 data DbConfig = DbConfig { dbAddress :: DbAddress , dbUsername :: Username , dbPassword :: Password } data AppConfig = AppConfig { dbConfig :: DbConfig , webServerConfig :: WebServerConfig , cronConfig :: CronConfig } 

ما هي بعض أمثلة المسارات من AppConfig ؟


  1. إلى DbConfigL Here .
  2. إلى WebServerConfigR (L Here) .
  3. إلى CronConfigR (R Here) .
  4. إلى DbAddressL (L Here) .

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


في الواقع ، النظر في مثال خدمة الويب القياسية لدينا. إذا أراد شخص ما الحصول على قيمة من النوع (Host, Port) ، فهل يجب أن يكون عنوان خادم قاعدة البيانات أو عنوان خادم الويب؟ من الأفضل عدم المخاطرة بذلك.


على أي حال ، دعنا نعبر عن ذلك في الكود:


 data MaybePath = NotFound | Conflict | Found Path deriving (Show) 

نحن نفصل بين NotFound و Conflict ، نظرًا لأن التعامل مع هذه الحالات يختلف اختلافًا جوهريًا: إذا حصلنا على NotFound في أحد فروع نوع المنتج لدينا ، فلن يكون من المؤلم العثور على القيمة المطلوبة في بعض الفروع الأخرى ، بينما يعني Conflict في أي فرع كامل فشل.


الآن نعتبر حالة خاصة لأنواع المنتجات (والتي ، كما اتفقنا ، نعتبرها أزواج). كيف تجد قيمة النوع المطلوب فيها؟ يمكنك إجراء بحث متكرر في كل مكون من مكونات زوج ، والحصول على النتائج p1 و p2 على التوالي ، ثم دمجها بطريقة أو بأخرى.


نظرًا لأننا نتحدث عن اختيار مثيلات timeclasses التي تحدث أثناء التحويل البرمجي ، فنحن في الواقع نحتاج إلى حسابات compiltime ، والتي يتم التعبير عنها في Haskell من خلال عمليات حسابية على الأنواع (حتى لو كانت الأنواع ممثلة من خلال المصطلحات التي أثيرت في الكون باستخدام DataKinds ). وفقًا لذلك ، يتم تمثيل هذه الوظيفة على الأنواع كعائلة من النوع:


 type family Combine p1 p2 where Combine ('Found path) 'NotFound = 'Found ('L path) Combine 'NotFound ('Found path) = 'Found ('R path) Combine 'NotFound 'NotFound = 'NotFound Combine _ _ = 'Conflict 

هذه الوظيفة تمثل عدة حالات:


  1. إذا نجحت إحدى عمليات البحث المتكررة ، وأدى الآخر إلى NotFound ، NotFound المسار من البحث الناجح ونلحق الدور في الاتجاه الصحيح.
  2. إذا تم إنهاء كل من عمليات البحث المتكررة مع NotFound ، فمن الواضح أن البحث بأكمله سينتهي مع NotFound .
  3. في أي حالة أخرى ، نحصل على Conflict .

سنقوم الآن بكتابة وظيفة على مستوى النصائح تأخذ part يمكن العثور عليه ، وتمثيلًا عامًا للنوع الذي سيتم فيه العثور على part ، والبحث:


 type family Search part (grecord :: k -> *) :: MaybePath where Search part (K1 _ part) = 'Found 'Here Search part (K1 _ other) = 'NotFound Search part (M1 _ _ x) = Search part x Search part (l :*: r) = Combine (Search part l) (Search part r) Search _ _ = 'NotFound 

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


GHas ، كل ما تبقى بالنسبة لنا هو إضافة معلمة إضافية إلى هذه الفئة ، المسؤولة عن المسار الموجود سابقًا ، والتي ستعمل على تحديد حالات محددة:


 class GHas (path :: Path) part grecord where gextract :: Proxy path -> grecord p -> part 

أضفنا أيضًا وسيطة إضافية لـ gextract بحيث يمكن للمترجم تحديد المثيل الصحيح للمسار المحدد (والذي يجب ذكره في توقيع الوظيفة لهذا).


الآن ، مثيلات الكتابة سهلة للغاية:


 instance GHas 'Here record (K1 i record) where gextract _ (K1 x) = x instance GHas path part record => GHas path part (M1 it record) where gextract proxy (M1 x) = gextract proxy x instance GHas path part l => GHas ('L path) part (l :*: r) where gextract _ (l :*: _) = gextract (Proxy :: Proxy path) l instance GHas path part r => GHas ('R path) part (l :*: r) where gextract _ (_ :*: r) = gextract (Proxy :: Proxy path) r 

في الواقع ، نحن ببساطة اختيار المثيل المطلوب على أساس المسار في path الذي قمنا بحسابه في وقت سابق.


كيف الآن لكتابة التنفيذ default لدينا extract :: record -> part وظيفة extract :: record -> part في فئة Has ؟ لدينا العديد من الشروط:


  1. يجب أن ينفذ record عام ، بحيث يمكن تطبيق الآلية العامة ، حتى نحصل على Generic record .
  2. يجب أن تجد وظيفة Search part من record (أو بالأحرى في التمثيل Generic record ، والذي يتم التعبير عنه Rep record ). في الكود ، يبدو هذا أكثر غرابة: Search part (Rep record) ~ 'Found path . يعني هذا السجل التقييد الذي يجب أن تساوي نتيجة Search part (Rep record) 'Found path لبعض path (والذي ، في الواقع ، مثير للاهتمام بالنسبة لنا)".
  3. يجب أن نكون قادرين على استخدام GHas مع part ، والتمثيل العام record path من الخطوة الأخيرة ، والتي تتحول إلى GHas path part (Rep record) .

سنلتقي مع الثوابتين الأخيرتين عدة مرات ، لذلك من المفيد أن نضعهما في مرادف منفصل:


 type SuccessfulSearch part record path = (Search part (Rep record) ~ 'Found path, GHas path part (Rep record)) 

بالنظر إلى هذا مرادف ، نحصل عليه


 class Has part record where extract :: record -> part default extract :: forall path. (Generic record, SuccessfulSearch part record path) => record -> part extract = gextract (Proxy :: Proxy path) . from 

الآن كل شيء!


باستخدام عام Has


للنظر في كل هذا في العمل ، سنكتب بعض الحالات العامة للدمى:


 instance SuccessfulSearch a (a0, a1) path => Has a (a0, a1) instance SuccessfulSearch a (a0, a1, a2) path => Has a (a0, a1, a2) instance SuccessfulSearch a (a0, a1, a2, a3) path => Has a (a0, a1, a2, a3) 

هنا SuccessfulSearch a (a0, ..., an) path هو المسؤول عن حقيقة أن a يحدث بين a0, ..., an مرة واحدة بالضبط.


قد يكون لدينا الآن لدينا حسن البالغ من العمر


 data AppConfig = AppConfig { dbConfig :: DbConfig , webServerConfig :: WebServerConfig , cronConfig :: CronConfig } 

ونريد إخراج Has DbConfig و Has WebServerConfig و Has CronConfig . يكفي تضمين DeriveAnyClass و DeriveAnyClass وإضافة الإعلان الصحيح DeriveAnyClass :


 data AppConfig = AppConfig { dbConfig :: DbConfig , webServerConfig :: WebServerConfig , cronConfig :: CronConfig } deriving (Generic, Has DbConfig, Has WebServerConfig, Has CronConfig) 

نحن محظوظون (أو كنا ثاقبة بما فيه الكفاية) لترتيب الوسائط لـ Has بحيث يأتي اسم النوع المتداخل أولاً ، حتى نتمكن من الاعتماد على آلية DeriveAnyClass لتقليل الخربشة.


السلامة تأتي أولاً


ماذا لو لم يكن لدينا أي نوع؟


 data AppConfig = AppConfig { dbConfig :: DbConfig , webServerConfig :: WebServerConfig } deriving (Generic, Has CronConfig) 

كلا ، حصلنا على خطأ في نقطة تعريف النوع:


 Spec.hs:35:24: error: • Couldn't match type ''NotFound' with ''Found path0' arising from the 'deriving' clause of a data type declaration • When deriving the instance for (Has CronConfig AppConfig) | 35 | } deriving (Generic, Has CronConfig) | ^^^^^^^^^^^^^^ 

ليست رسالة الخطأ الودية ، ولكن حتى من ذلك لا يزال بإمكانك فهم ما المشكلة: التردد الفردي NotFound التردد الفردي CronConfig .


ماذا لو كان لدينا العديد من الحقول من نفس النوع؟


 data AppConfig = AppConfig { prodDbConfig :: DbConfig , qaDbConfig :: DbConfig , webServerConfig :: WebServerConfig , cronConfig :: CronConfig } deriving (Generic, Has DbConfig) 

كلا ، كما هو متوقع:


 Spec.hs:37:24: error: • Couldn't match type ''Conflict' with ''Found path0' arising from the 'deriving' clause of a data type declaration • When deriving the instance for (Has DbConfig AppConfig) | 37 | } deriving (Generic, Has DbConfig) | ^^^^^^^^^^^^ 

يبدو أن كل شيء جيد حقًا.


لتلخيص


لذلك ، سنحاول صياغة الطريقة المقترحة لفترة وجيزة.


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


  1. نقوم بتشفير القواعد العودية في شكل نوع بيانات استقرائي T
  2. سنقوم بكتابة وظيفة على أنواع (في شكل عائلة من النوع) لحساب أولي للقيمة v هذا النوع T (أو ، من حيث Haskell ، اكتب v النوع T - أين أنواعي التابعة) ، والتي تصف التسلسل المحدد للخطوات التي يجب اتخاذها.
  3. استخدم هذه v كوسيطة إضافية إلى مساعد Generic لتحديد التسلسل المحدد للحالات التي تتطابق الآن مع قيم v .

حسنا ، هذا كل شيء!


في الوظائف التالية ، سننظر في بعض الامتدادات الأنيقة (وكذلك القيود الأنيقة) لهذا النهج.


نعم و نعم من المثير للاهتمام تتبع تسلسل تعميماتنا.


  1. بدأت مع Env -> Foo .
  2. ليست عامة بما فيه الكفاية ، اختتم في Reader Env monad.
  3. ليس عام بما فيه الكفاية ، إعادة كتابة مع MonadReader Env m .
  4. ليست عامة بما فيه الكفاية ، MonadReader rm, HasEnv r كتابة MonadReader rm, HasEnv r .
  5. ليس بالقدر الكافي ، MonadReader rm, Has Env r نكتب MonadReader rm, Has Env r ونضيف الأدوية العامة بحيث يقوم المترجم MonadReader rm, Has Env r كل شيء هناك.
  6. الآن القاعدة.

Source: https://habr.com/ru/post/ar471052/


All Articles