Can i haz? Consider the Has pattern

Hi, Habr.







Today we will consider such an FP pattern as Has



-class. This is a rather interesting thing for several reasons: firstly, we will once again make sure that there are patterns in the FP. Secondly, it turns out that the implementation of this pattern can be entrusted to the machine, which turned out to be a rather interesting trick with typeclasses (and the Hackage library), which once again demonstrates the practical usefulness of type system extensions outside of Haskell 2010 and IMHO is much more interesting than this pattern itself. Thirdly, an occasion for cats.







image







However, perhaps, it’s worthwhile to start with a description of what a Has



class is, all the more so since there wasn’t any short (and, especially, Russian-speaking) description.







So, how does the Haskell solve the problem of managing some global read-only environment that several different functions need? How, for example, is the global configuration of the application expressed?







The most obvious and direct solution is that if a function needs a value of type Env



, then you can simply pass a value of type Env



to this function!







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





However, unfortunately, such a function is not very composable, especially compared to some other objects that we are used to in the Haskell. For example, compared with monads.







Actually, a more generalized solution is to wrap functions that need access to the Env



environment in the 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 ...
      
      





This can be generalized even more, for which it is enough to use the MonadReader MonadReader



and just change the type of function:







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





Now it doesn’t matter to us exactly which monadic stack we are in, as long as we can get a value of type Env



from it (and we explicitly express this in the type of our function). We don’t care if the whole stack has any other features like IO



or error handling through MonadError



:







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





And, by the way, a little higher, I actually lied when I said that the approach of explicitly passing an argument to a function is not as composable as monads: the “partially applied” functional type r ->



is a monad, and, moreover, it is quite a legitimate instance of the MonadReader r



class. The development of appropriate intuition is offered to the reader as an exercise.







In any case, this is a good step towards modularity. Let's see where he leads us.







Why has



Let us work on some kind of web service, which, among other things, may have the following components:









Each of these modules can have its own configuration:









We can say that the overall configuration of the entire application is a combination of all these settings (and, probably, something else).







For simplicity, suppose that the API of each module consists of only one function:









Each of these features requires an appropriate configuration. We already learned that MonadReader



is a good practice, but what will be the type of environment?







The most obvious solution would be something like







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





Most likely, these features will require MonadIO



and, possibly, something else, but this is not so important for our discussion.







In fact, we just did a terrible thing. Why? Well, offhand:







  1. We have added an unnecessary connection between completely different components. Ideally, the database layer should not know anything about some kind of web server. And, of course, we should not recompile the module for working with the database when changing the list of configuration options for the web server.
  2. This will not work at all if we cannot edit the source code for some of the modules. For example, what if the cron-module is implemented in some third-party library that does not know anything about our particular user case?
  3. We added opportunities to make a mistake. For example, what is serverAddress



    ? Is this the address that the web server should listen to, or is the address of the database server? Using one large type for all options increases the chance of such collisions.
  4. We can no longer conclude from a single glance at function signatures which modules use which part of the configuration. Everything has access to everything!


So what is the solution for this all? As you might guess from the title of the article, this







Has



Pattern



In fact, each module does not care about the type of the entire environment, as long as this type has the data needed for the module. This is easiest to show with an example.







Consider a module for working with a database and suppose that it defines a type that contains all the configuration that a module needs:







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





Has



-pattern is represented as the following typeclass:







 class HasDbConfig rec where getDbConfig :: rec -> DbConfig
      
      





Then the setupDatabase



type will look like







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





and in the body of the function we just have to use asks $ foo . getDbConfig



asks $ foo . getDbConfig



where we used to use asks foo



because of the extra abstraction layer we just added.







Similarly, we will have the HasWebServerConfig



and HasCronConfig



.







What if some function uses two different modules? Just compatible constraints!







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





What about the implementations of these typeclasses?







We still have AppConfig



at the highest level of our application (just now the modules do not know about it), and for it we can write:







 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
      
      





It looks good so far. However, this approach has one problem - too much writing , and we will examine it in more detail in the next post.








All Articles