驴Puedo hacer? Considere el patr贸n Has

Hola Habr


Hoy consideraremos un patr贸n de FP como Has -class. Esto es algo bastante interesante por varias razones: en primer lugar, nos aseguraremos una vez m谩s de que haya patrones en el FP. En segundo lugar, resulta que la implementaci贸n de este patr贸n puede confiarse a la m谩quina, lo que result贸 ser un truco bastante interesante con las clases de tipos (y la biblioteca Hackage), que una vez m谩s demuestra la utilidad pr谩ctica de las extensiones del sistema de tipos fuera de Haskell 2010 e IMHO es mucho m谩s interesante que este patr贸n en s铆. En tercer lugar, una ocasi贸n para gatos.


imagen


Sin embargo, quiz谩s valga la pena comenzar con una descripci贸n de lo que es una clase Has , especialmente porque no hab铆a una descripci贸n breve (y, especialmente, una en ruso) de la misma.


Entonces, 驴c贸mo resuelve Haskell el problema de administrar un entorno global de solo lectura que necesitan varias funciones diferentes? 驴C贸mo, por ejemplo, se expresa la configuraci贸n global de la aplicaci贸n?


La soluci贸n m谩s obvia y directa es que si una funci贸n necesita un valor de tipo Env , 隆simplemente puede pasar un valor de tipo Env a esta funci贸n!


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

Sin embargo, desafortunadamente, dicha funci贸n no es muy componible, especialmente en comparaci贸n con algunos otros objetos a los que estamos acostumbrados en Haskell. Por ejemplo, en comparaci贸n con las m贸nadas.


En realidad, una soluci贸n m谩s generalizada es envolver funciones que necesitan acceso al entorno Env en la m贸nada 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 ... 

Esto se puede generalizar a煤n m谩s, para lo cual es suficiente usar la clase de tipo MonadReader y simplemente cambiar el tipo de funci贸n:


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

Ahora no nos importa exactamente en qu茅 pila mon谩dica estamos, siempre y cuando podamos obtener el valor del tipo Env (y expresamente lo expresamos en el tipo de nuestra funci贸n). No nos importa si toda la pila tiene otras caracter铆sticas como IO o manejo de errores a trav茅s de MonadError :


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

Y, por cierto, un poco m谩s alto, en realidad ment铆 cuando dije que el enfoque de pasar expl铆citamente un argumento a una funci贸n no es tan componible como las m贸nadas: el tipo funcional "parcialmente aplicado" r -> es una m贸nada, y, adem谩s, es bastante una instancia leg铆tima de la clase MonadReader r . El desarrollo de la intuici贸n apropiada se ofrece al lector como un ejercicio.


En cualquier caso, este es un buen paso hacia la modularidad. Veamos a d贸nde nos lleva.


Por qu茅 tiene


Trabajemos en alg煤n tipo de servicio web, que, entre otras cosas, puede tener los siguientes componentes:


  • Capa de acceso a la base de datos
  • servidor web
  • temporizador activado m贸dulo similar al cron.

Cada uno de estos m贸dulos puede tener su propia configuraci贸n:


  • detalles de acceso a la base de datos,
  • host y puerto para el servidor web,
  • intervalo de operaci贸n del temporizador.

Podemos decir que la configuraci贸n general de toda la aplicaci贸n es una combinaci贸n de todas estas configuraciones (y, probablemente, algo m谩s).


Para simplificar, suponga que la API de cada m贸dulo consta de una sola funci贸n:


  • setupDatabase
  • startServer
  • runCronJobs

Cada una de estas caracter铆sticas requiere una configuraci贸n adecuada. Ya aprendimos que MonadReader es una buena pr谩ctica, pero 驴cu谩l ser谩 el tipo de entorno?


La soluci贸n m谩s obvia ser铆a algo as铆 como


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

Lo m谩s probable es que estas caracter铆sticas requieran MonadIO y, posiblemente, algo m谩s, pero esto no es tan importante para nuestra discusi贸n.


De hecho, acabamos de hacer algo terrible. Por qu茅 Bueno, de improviso:


  1. Hemos agregado una conexi贸n innecesaria entre componentes completamente diferentes. Idealmente, la capa DB no deber铆a saber nada sobre alg煤n tipo de servidor web. Y, por supuesto, no debemos volver a compilar el m贸dulo para trabajar con la base de datos al cambiar la lista de opciones de configuraci贸n para el servidor web.
  2. Esto no funcionar谩 en absoluto si no podemos editar el c贸digo fuente de algunos de los m贸dulos. Por ejemplo, 驴qu茅 debo hacer si el m贸dulo cron est谩 implementado en una biblioteca de terceros que no sabe nada sobre nuestro caso de usuario espec铆fico?
  3. Agregamos oportunidades para cometer un error. Por ejemplo, 驴qu茅 es serverAddress ? 驴Es esta la direcci贸n que el servidor web debe escuchar, o es la direcci贸n del servidor de la base de datos? El uso de un tipo grande para todas las opciones aumenta la posibilidad de tales colisiones.
  4. Ya no podemos concluir de un vistazo a las firmas de funciones qu茅 m贸dulos usan qu茅 parte de la configuraci贸n. 隆Todo tiene acceso a todo!

Entonces, 驴cu谩l es la soluci贸n para todo esto? Como se puede adivinar por el t铆tulo del art铆culo, esto


Has patr贸n


De hecho, a cada m贸dulo no le importa el tipo de entorno completo, siempre que este tipo tenga los datos necesarios para el m贸dulo. Esto es m谩s f谩cil de mostrar con un ejemplo.


Considere un m贸dulo para trabajar con una base de datos y suponga que define un tipo que contiene toda la configuraci贸n que necesita el m贸dulo:


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

Has -pattern se representa como la siguiente clase de tipo:


 class HasDbConfig rec where getDbConfig :: rec -> DbConfig 

Entonces el tipo setupDatabase se ver谩 as铆


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

y en el cuerpo de la funci贸n solo tenemos que usar asks $ foo . getDbConfig asks $ foo . getDbConfig donde usamos asks foo antes, debido a la capa de abstracci贸n adicional que acabamos de agregar.


Del mismo modo, tendremos las HasWebServerConfig HasCronConfig y HasCronConfig .


驴Qu茅 pasa si alguna funci贸n usa dos m贸dulos diferentes? 隆Solo restricciones compatibles!


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

驴Qu茅 pasa con las implementaciones de estas clases de tipos?


Todav铆a tenemos AppConfig en el nivel m谩s alto de nuestra aplicaci贸n (justo ahora los m贸dulos no lo saben), y para ello podemos escribir:


 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 

Se ve bien hasta ahora. Sin embargo, este enfoque tiene un problema: demasiada escritura , y lo examinaremos con m谩s detalle en la pr贸xima publicaci贸n.

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


All Articles