مرحبا يا هبر.
في المرة الأخيرة ، وصفنا " 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 الثابت. هذه المهمة لديها هيكل حثي جيد:
- كل نوع له نفسه: يتم تنفيذ Has record recordبطريقة تافهة (extract = id).
- إذا كان recordعبارة عن منتج لأنواعrec1وrec2، فسيتم تنفيذHas part recordإذا كان فقط إذا كانHas part rec1أوHas part rec2.
- إذا كان 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 
- K1يتوافق مع القضية الأساسية.
- M1- البيانات الوصفية الخاصة بالأدوية التي لا نحتاج إليها في مهمتنا ، لذلك نحن ببساطة نتجاهلها ونتعرف عليها.
- المثيل الأول لنوع المنتج l :*: rيتوافق مع الحالة عندما يكون للجزء "الأيسر" من المنتج قيمةpartالنوع الذي نحتاجه (ربما ، بشكل متكرر).
- وبالمثل ، فإن المثيل الثاني لنوع المنتج l :*: rيتوافق مع الحالة التي يكون فيها للجزء "الأيمن" من المنتج قيمةpartالكتابة الذي نحتاج إليه (بشكل طبيعي ، أيضًا ، بشكل متكرر).
نحن ندعم فقط أنواع المنتجات هنا. انطباعي الشخصي هو أن الكميات لا تستخدم غالبًا في سياقات MonadReader والفئات المماثلة ، لذلك يمكن إهمالها لتبسيط الاعتبار.
بالإضافة إلى ذلك ، من المفيد أن نلاحظ أن كل منها يمكن تمثيل نوع المنتج -ary (a1, ..., an) على أنه تكوين أزواج (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 ؟
- إلى DbConfig⟶L Here.
- إلى WebServerConfig⟶R (L Here).
- إلى CronConfig⟶R (R Here).
- إلى DbAddress⟶L (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 
هذه الوظيفة تمثل عدة حالات:
- إذا نجحت إحدى عمليات البحث المتكررة ، وأدى الآخر إلى NotFound،NotFoundالمسار من البحث الناجح ونلحق الدور في الاتجاه الصحيح.
- إذا تم إنهاء كل من عمليات البحث المتكررة مع NotFound، فمن الواضح أن البحث بأكمله سينتهي معNotFound.
- في أي حالة أخرى ، نحصل على 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 ؟ لدينا العديد من الشروط:
- يجب أن ينفذ recordعام ، بحيث يمكن تطبيق الآلية العامة ، حتى نحصل علىGeneric record.
- يجب أن تجد وظيفة Searchpartمنrecord(أو بالأحرى في التمثيلGenericrecord، والذي يتم التعبير عنهRep record). في الكود ، يبدو هذا أكثر غرابة:Search part (Rep record) ~ 'Found path. يعني هذا السجل التقييد الذي يجب أن تساوي نتيجةSearch part (Rep record)'Found pathلبعضpath(والذي ، في الواقع ، مثير للاهتمام بالنسبة لنا)".
- يجب أن نكون قادرين على استخدام GHasمعpart، والتمثيل العامrecordpathمن الخطوة الأخيرة ، والتي تتحول إلى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 ، ونريد عرض مثيلاته تلقائيًا وفقًا لبعض القواعد العودية. بعد ذلك ، يمكننا تجنب الغموض (والتعبير عمومًا عن هذه القواعد إذا كانت غير بديهية ولا تنسجم مع الآلية القياسية لحل الحالات) على النحو التالي:
- نقوم بتشفير القواعد العودية في شكل نوع بيانات استقرائي T
- سنقوم بكتابة وظيفة على أنواع (في شكل عائلة من النوع) لحساب أولي للقيمة vهذا النوعT(أو ، من حيث Haskell ، اكتبvالنوعT- أين أنواعي التابعة) ، والتي تصف التسلسل المحدد للخطوات التي يجب اتخاذها.
- استخدم هذه vكوسيطة إضافية إلى مساعدGenericلتحديد التسلسل المحدد للحالات التي تتطابق الآن مع قيمv.
حسنا ، هذا كل شيء!
في الوظائف التالية ، سننظر في بعض الامتدادات الأنيقة (وكذلك القيود الأنيقة) لهذا النهج.
نعم و نعم من المثير للاهتمام تتبع تسلسل تعميماتنا.
- بدأت مع Env -> Foo.
- ليست عامة بما فيه الكفاية ، اختتم في Reader Envmonad.
- ليس عام بما فيه الكفاية ، إعادة كتابة مع MonadReader Env m.
- ليست عامة بما فيه الكفاية ، MonadReader rm, HasEnv rكتابةMonadReader rm, HasEnv r.
- ليس بالقدر الكافي ، MonadReader rm, Has Env rنكتبMonadReader rm, Has Env rونضيف الأدوية العامة بحيث يقوم المترجمMonadReader rm, Has Env rكل شيء هناك.
- الآن القاعدة.