Puis-je Haz? Considérez le modèle Has

Salut, Habr.


Aujourd'hui, nous allons considérer un modèle de PF comme Has -class. C'est une chose assez intéressante pour plusieurs raisons: premièrement, nous allons encore une fois nous assurer qu'il y a des modèles dans le FP. Deuxièmement, il s'avère que la mise en œuvre de ce modèle peut être confiée à la machine, ce qui s'est avéré être une astuce plutôt intéressante avec les classes de types (et la bibliothèque Hackage), ce qui démontre une fois de plus l'utilité pratique des extensions de système de type en dehors de Haskell 2010 et à mon humble avis est beaucoup plus intéressant que ce modèle lui-même. Troisièmement, une occasion pour les chats.


image


Cependant, il vaut peut-être la peine de commencer par une description de ce qu'est une classe Has , d'autant plus qu'il n'y en avait pas de courte description (et, surtout, en russe).


Alors, comment Haskell résout-il le problème de la gestion d'un environnement global en lecture seule dont plusieurs fonctions différentes ont besoin? Comment, par exemple, la configuration globale de l'application est-elle exprimée?


La solution la plus évidente et la plus directe est que si une fonction a besoin d'une valeur de type Env , vous pouvez simplement passer une valeur de type Env à cette fonction!


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

Cependant, malheureusement, une telle fonction n'est pas très composable, surtout par rapport à d'autres objets auxquels nous sommes habitués dans le Haskell. Par exemple, par rapport aux monades.


En fait, une solution plus généralisée consiste à encapsuler les fonctions qui ont besoin d'accéder à l'environnement Env dans la monade Reader Env :


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

Cela peut être encore plus généralisé, pour lequel il suffit d'utiliser la classe de MonadReader et de simplement changer le type de fonction:


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

Maintenant, nous ne nous soucions plus vraiment de la pile monadique dans laquelle nous nous trouvons, tant que nous pouvons en obtenir une valeur de type Env (et nous l'exprimons explicitement dans le type de notre fonction). Nous ne nous soucions pas si la pile entière a d'autres fonctionnalités comme les IO / IO ou la gestion des erreurs via MonadError :


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

Et, en passant, un peu plus haut, j'ai menti en disant que l'approche de passer explicitement un argument à une fonction n'est pas aussi composable que les monades: le type fonctionnel "partiellement appliqué" r -> est une monade, et, de plus, c'est assez une instance légitime de la classe MonadReader r . Le développement d'une intuition appropriée est proposé au lecteur comme un exercice.


En tout cas, c'est un bon pas vers la modularité. Voyons où il nous mène.


Pourquoi


Laissez-nous travailler sur une sorte de service Web, qui, entre autres, peut avoir les composants suivants:


  • Couche d'accès DB
  • serveur web
  • module de type cron activé par minuterie.

Chacun de ces modules peut avoir sa propre configuration:


  • les détails de l'accès à la base de données,
  • hôte et port pour le serveur web,
  • intervalle de fonctionnement de la minuterie.

Nous pouvons dire que la configuration globale de l'application entière est une combinaison de tous ces paramètres (et, probablement, autre chose).


Par souci de simplicité, supposons que l'API de chaque module se compose d'une seule fonction:


  • setupDatabase
  • startServer
  • runCronJobs

Chacune de ces fonctionnalités nécessite une configuration appropriée. Nous avons déjà appris que MonadReader est une bonne pratique, mais quel sera le type d'environnement?


La solution la plus évidente serait quelque chose comme


 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 () 

Très probablement, ces fonctionnalités nécessiteront MonadIO et, éventuellement, autre chose, mais ce n'est pas si important pour notre discussion.


En fait, nous venons de faire une chose terrible. Pourquoi? Eh bien, désinvolte:


  1. Nous avons ajouté une connexion inutile entre des composants complètement différents. Idéalement, la couche DB ne devrait rien savoir d'une sorte de serveur Web. Et, bien sûr, nous ne devons pas recompiler le module pour travailler avec la base de données lors de la modification de la liste des options de configuration du serveur Web.
  2. Cela ne fonctionnera pas du tout si nous ne pouvons pas éditer le code source de certains modules. Par exemple, que dois-je faire si le module cron est implémenté dans une bibliothèque tierce qui ne sait rien de notre cas d'utilisateur spécifique?
  3. Nous avons ajouté des occasions de faire une erreur. Par exemple, qu'est-ce que serverAddress ? Est-ce l'adresse que le serveur Web doit écouter ou l'adresse du serveur de base de données? L'utilisation d'un grand type pour toutes les options augmente les risques de telles collisions.
  4. Nous ne pouvons plus conclure d'un coup d'œil aux signatures de fonction quels modules utilisent quelle partie de la configuration. Tout a accès à tout!

Alors, quelle est la solution pour tout cela? Comme vous pouvez le deviner d'après le titre de l'article, cette


Has motif


En fait, chaque module ne se soucie pas du type de l'environnement entier, tant que ce type a les données nécessaires pour le module. C'est plus facile à montrer avec un exemple.


Considérez un module pour travailler avec une base de données et supposez qu'il définit un type qui contient toute la configuration dont le module a besoin:


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

Has -pattern est représenté par la classe de types suivante:


 class HasDbConfig rec where getDbConfig :: rec -> DbConfig 

Ensuite, le type setupDatabase ressemblera


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

et dans le corps de la fonction, il suffit d'utiliser asks $ foo . getDbConfig asks $ foo . getDbConfig où nous asks $ foo . getDbConfig auparavant asks foo , en raison de la couche supplémentaire d'abstraction que nous venons d'ajouter.


De même, nous aurons les HasWebServerConfig et HasCronConfig .


Et si une fonction utilise deux modules différents? Conforme juste compatible!


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

Qu'en est-il des implémentations de ces classes de types?


Nous avons encore AppConfig au plus haut niveau de notre application (pour l'instant les modules ne le savent pas), et pour cela nous pouvons écrire:


 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 

Il semble bon jusqu'à présent. Cependant, cette approche a un problème - trop d'écriture , et nous l'examinerons plus en détail dans le prochain post.

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


All Articles