Posso haz? Considere o padrão Tem

Oi Habr.


Hoje consideraremos um padrão FP como Has -class. Isso é interessante por várias razões: em primeiro lugar, garantiremos mais uma vez que existem padrões no PF. Em segundo lugar, verifica-se que a implementação desse padrão pode ser confiada à máquina, o que acabou sendo um truque bastante interessante com as classes de tipos (e a biblioteca Hackage), que mais uma vez demonstra a utilidade prática das extensões de sistemas de tipos fora do Haskell 2010 e IMHO é muito mais interessante do que esse próprio padrão. Em terceiro lugar, uma ocasião para gatos.


imagem


No entanto, talvez valha a pena começar com uma descrição do que é uma classe Has , especialmente porque não havia uma descrição curta (e, principalmente, uma em russo).


Então, como o Haskell resolve o problema de gerenciar um ambiente global somente leitura que várias funções diferentes precisam? Como, por exemplo, é expressa a configuração global do aplicativo?


A solução mais óbvia e direta é que, se uma função precisar de um valor do tipo Env , você poderá simplesmente passar um valor do tipo Env para essa função!


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

No entanto, infelizmente, essa função não é muito passível de composição, principalmente se comparada a outros objetos com os quais estamos acostumados no Haskell. Por exemplo, comparado com mônadas.


Na verdade, uma solução mais generalizada é agrupar funções que precisam acessar o ambiente Env na 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 ... 

Isso pode ser generalizado ainda mais, para o qual basta usar a classe MonadReader e apenas alterar o tipo de função:


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

Agora, não nos importa exatamente em que pilha monádica estamos, desde que possamos obter o valor do tipo Env partir dela (e expressamos isso explicitamente no tipo de nossa função). Não nos importamos se a pilha inteira possui outros recursos, como IO / IO ou tratamento de erros através do MonadError :


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

E, a propósito, um pouco mais alto, eu realmente menti quando disse que a abordagem de passar explicitamente um argumento para uma função não é tão compostável quanto as mônadas: o tipo funcional "parcialmente aplicado" r -> é uma mônada e, além disso, é bastante uma instância legítima da classe MonadReader r . O desenvolvimento de intuição apropriada é oferecido ao leitor como um exercício.


De qualquer forma, este é um bom passo para a modularidade. Vamos ver aonde ele nos leva.


Por que


Vamos trabalhar em algum tipo de serviço da web que, entre outras coisas, pode ter os seguintes componentes:


  • Camada de acesso ao banco de dados
  • servidor web
  • temporizador ativado módulo cron.

Cada um desses módulos pode ter sua própria configuração:


  • detalhes de acesso ao banco de dados,
  • host e porta para o servidor web,
  • intervalo de operação do temporizador.

Podemos dizer que a configuração geral de todo o aplicativo é uma combinação de todas essas configurações (e, provavelmente, outra coisa).


Para simplificar, suponha que a API de cada módulo consista em apenas uma função:


  • setupDatabase
  • startServer
  • runCronJobs

Cada um desses recursos requer uma configuração apropriada. Nós já aprendemos que o MonadReader é uma boa prática, mas qual será o tipo de ambiente?


A solução mais óbvia seria algo 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 () 

Muito provavelmente, esses recursos exigirão o MonadIO e, possivelmente, algo mais, mas isso não é tão importante para a nossa discussão.


De fato, fizemos uma coisa terrível. Porque Bem, de imediato:


  1. Adicionamos uma conexão desnecessária entre componentes completamente diferentes. Idealmente, a camada DB não deve saber nada sobre algum tipo de servidor web. E, é claro, não devemos recompilar o módulo para trabalhar com o banco de dados ao alterar a lista de opções de configuração para o servidor da web.
  2. Isso não funcionará se não pudermos editar o código fonte de alguns dos módulos. Por exemplo, o que devo fazer se o módulo cron for implementado em alguma biblioteca de terceiros que não saiba nada sobre o nosso caso de usuário específico?
  3. Adicionamos oportunidades para cometer um erro. Por exemplo, o que é serverAddress ? Esse é o endereço que o servidor da Web deve escutar ou é o endereço do servidor de banco de dados? O uso de um tipo grande para todas as opções aumenta a chance de tais colisões.
  4. Não podemos mais concluir, de relance, nas assinaturas das funções que módulos usam qual parte da configuração. Tudo tem acesso a tudo!

Então, qual é a solução para tudo isso? Como você pode imaginar, no título do artigo, este


Has padrão


De fato, cada módulo não se importa com o tipo de ambiente inteiro, desde que esse tipo possua os dados necessários para o módulo. Isso é mais fácil de mostrar com um exemplo.


Considere um módulo para trabalhar com um banco de dados e suponha que ele defina um tipo que contenha toda a configuração que o módulo precisa:


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

Has -padrão é representado como a seguinte classe de tipo:


 class HasDbConfig rec where getDbConfig :: rec -> DbConfig 

Em seguida, o tipo setupDatabase parecerá


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

e no corpo da função, apenas precisamos usar o asks $ foo . getDbConfig asks $ foo . getDbConfig onde usamos o asks foo antes, devido à camada de abstração extra que acabamos de adicionar.


Da mesma forma, teremos as HasWebServerConfig HasCronConfig e HasCronConfig .


E se alguma função usar dois módulos diferentes? Apenas construções compatíveis!


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

E as implementações dessas classes de tipo?


Ainda temos o AppConfig no nível mais alto de nosso aplicativo (agora os módulos ainda não sabem disso) e, para isso, podemos escrever:


 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 

Parece bom até agora. No entanto, essa abordagem tem um problema - muita escrita , e a examinaremos em mais detalhes no próximo post.

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


All Articles