هل استطيع هاز؟ النظر في نمط لديه

مرحبا يا هبر.


اليوم سننظر في نمط FP مثل Has -class. هذا شيء مثير للاهتمام إلى حد ما لعدة أسباب: أولاً ، سوف نتأكد مرة أخرى من وجود أنماط في FP. وثانياً ، اتضح أن تنفيذ هذا النمط يمكن أن يعهد به إلى الجهاز ، والذي اتضح أنه خدعة مثيرة للاهتمام إلى حد ما مع أشكال الطباعة (ومكتبة Hackage) ، مما يدل مرة أخرى على الفائدة العملية لملحقات نظام الكتابة خارج Haskell 2010 و IMHO أكثر إثارة للاهتمام من هذا النمط نفسه. ثالثا ، مناسبة للقطط.


صورة


ومع ذلك ، ربما يكون من المفيد أن نبدأ بوصف Has الفصل الدراسي ، على الأخص لأنه لم يكن هناك أي وصف مختصر (وخاصة اللغة الروسية).


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


الحل الأكثر وضوحا ومباشرة هو أنه إذا كانت الوظيفة تحتاج إلى قيمة من النوع Env ، فيمكنك ببساطة تمرير قيمة type Env إلى هذه الوظيفة!


 iNeedEnv :: Env -> Foo iNeedEnv env = -- ,  env    

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


في الواقع ، هناك حل أكثر تعميماً يتمثل في التفاف الوظائف التي تحتاج إلى الوصول إلى بيئة Env في برنامج Reader Env monad:


 import Control.Monad.Reader data Env = Env { someConfigVariable :: Int , otherConfigVariable :: [String] } iNeedEnv :: Reader Env Foo iNeedEnv = do --    : env <- ask --  c    : theInt <- asks someConfigVariable ... 

يمكن تعميم هذا الأمر أكثر ، وهو ما يكفي لاستخدام محرف MonadReader وتغيير نوع الوظيفة:


 iNeedEnv :: MonadReader Env m => m Foo iNeedEnv = --     ,    

الآن لا يهمنا بالضبط المكدس الأحادي الذي نتواجد فيه ، طالما يمكننا الحصول على قيمة النوع Env منه (ونعبر بوضوح عن ذلك في نوع وظيفتنا). لا يهمنا إذا كانت المجموعة بأكملها بها أي ميزات أخرى مثل IO أو معالجة الأخطاء من خلال MonadError :


 someCaller :: (MonadIO m, MonadReader Env m, MonadError Err m) => m Bar someCaller = do theFoo <- iNeedEnv ... 

وبالمناسبة ، أعلى قليلاً ، لقد كذبت بالفعل عندما قلت إن أسلوب تمرير الحجة صراحةً إلى دالة ما ليس قابلاً للتركيب مثل monad: النوع الوظيفي "المطبق جزئيًا" r -> هو monad ، علاوة على ذلك ، إنه أحادي مثيل شرعي MonadReader r . يتم تقديم تطوير الحدس المناسب للقارئ كتمرين.


في أي حال ، هذه خطوة جيدة نحو نمطية. دعونا نرى أين يقودنا.


لماذا لديها


دعونا نعمل على نوع من خدمات الويب ، والتي ، من بين أشياء أخرى ، قد تحتوي على المكونات التالية:


  • طبقة الوصول DB
  • خادم الويب
  • الموقت تنشيط وحدة تشبه كرون.

يمكن أن يكون لكل من هذه الوحدات التكوين الخاص بها:


  • تفاصيل الوصول إلى قاعدة البيانات ،
  • المضيف والمنفذ لخادم الويب ،
  • فاصل العملية الموقت.

يمكننا القول أن التكوين العام للتطبيق بأكمله هو مزيج من كل هذه الإعدادات (وربما شيء آخر).


للبساطة ، افترض أن واجهة برمجة التطبيقات لكل وحدة تتكون من وظيفة واحدة فقط:


  • setupDatabase
  • startServer
  • runCronJobs

كل من هذه الميزات يتطلب التكوين المناسب. لقد تعلمنا بالفعل أن MonadReader هي ممارسة جيدة ، ولكن ماذا سيكون نوع البيئة؟


الحل الأكثر وضوحا سيكون شيء من هذا القبيل


 data AppConfig = AppConfig { dbCredentials :: DbCredentials , serverAddress :: (Host, Port) , cronPeriodicity :: Ratio Int } setupDatabase :: MonadReader AppConfig m => m Db startServer :: MonadReader AppConfig m => m Server runCronJobs :: MonadReader AppConfig m => m () 

على الأرجح ، ستتطلب هذه الميزات MonadIO ، وربما شيء آخر ، ولكن هذا ليس مهمًا جدًا لمناقشتنا.


في الواقع ، لقد فعلنا شيئًا فظيعًا. لماذا؟ حسنا ، مرتجلا:


  1. لقد أضفنا اتصالًا غير ضروري بين مكونات مختلفة تمامًا. من الناحية المثالية ، يجب ألا تعرف طبقة الديسيبل أي شيء عن نوع من خادم الويب. وبالطبع ، لا ينبغي لنا إعادة ترجمة الوحدة النمطية للعمل مع قاعدة البيانات عند تغيير قائمة خيارات التكوين لخادم الويب.
  2. لن ينجح هذا على الإطلاق إذا لم نتمكن من تعديل الكود المصدري لبعض الوحدات. على سبيل المثال ، ماذا علي أن أفعل إذا تم تنفيذ وحدة cron في بعض مكتبات الطرف الثالث التي لا تعرف أي شيء عن حالة المستخدم الخاصة بنا؟
  3. أضفنا الفرص لارتكاب خطأ. على سبيل المثال ، ما هو serverAddress ؟ هل هذا هو العنوان الذي يجب على خادم الويب الاستماع إليه ، أم هو عنوان خادم قاعدة البيانات؟ استخدام نوع واحد كبير لجميع الخيارات يزيد من فرصة حدوث مثل هذه التصادمات.
  4. لم يعد بإمكاننا الاستنتاج من التواقيع الدالة التي تستخدم الوحدات النمطية أي جزء من التكوين. كل شيء لديه حق الوصول إلى كل شيء!

إذن ما هو الحل لهذا كله؟ كما قد تخمن من عنوان المقال ، هذا


Has نمط


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


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


 data DbConfig = DbConfig { dbCredentials :: DbCredentials , ... } 

يمثل Has -pattern كما typeclass التالية:


 class HasDbConfig rec where getDbConfig :: rec -> DbConfig 

ثم setupDatabase نوع setupDatabase


 setupDatabase :: (MonadReader rm, HasDbConfig r) => m Db 

وفي جسد الوظيفة ، علينا فقط أن asks $ foo . getDbConfig asks $ foo . getDbConfig حيث استخدمنا asks foo قبل ، بسبب طبقة التجريد الإضافية التي أضفناها للتو.


على نحو مماثل ، سيكون لدينا HasWebServerConfig و HasCronConfig .


ماذا لو كانت بعض الوظائف تستخدم وحدتين مختلفتين؟ فقط المتوافقة constrates!


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

ماذا عن تطبيقات هذه الحروف؟


لا يزال لدينا AppConfig على أعلى مستوى من طلبنا (الآن فقط الوحدات لا تعرف عنها) ، ولهذا يمكننا أن نكتب:


 data AppConfig = AppConfig { dbConfig :: DbConfig , webServerConfig :: WebServerConfig , cronConfig :: CronConfig } instance HasDbConfig AppConfig where getDbConfig = dbConfig instance HasWebServerConfig AppConfig where getWebServerConfig = webServerCOnfig instance HasCronConfig AppConfig where getCronConfig = cronConfig 

انها تبدو جيدة حتى الآن. ومع ذلك ، فإن هذا النهج لديه مشكلة واحدة - الكثير من الكتابة ، وسوف ندرسها بمزيد من التفصيل في المنشور التالي.

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


All Articles