Haskell Dependency Injection: Problem Solving Idiomatically

What is the Haskell idiomatic dependency injection solution?

For example, suppose you have a frobby interface, and you need to pass an instance matching frobby around (there may be several varieties of these instances, say foo and bar ).

Typical operations:

  • which take some value of X and return some value of Y For example, it could be a database accessory containing an SQL query and a connector and returning a data set. You may need to implement postgres, mysql, and a mock test system.

  • which take some value of Z and return a closure related to Z , specialized for a given style of foo or bar selected at runtime.

One person solved the problem as follows:

http://mikehadlow.blogspot.com/2011/05/dependency-injection-haskell-style.html

But I do not know what this way of managing this task is.

+45
design haskell
Jan 14 '13 at 21:46
source share
5 answers

I think the correct answer is here, and I will probably get a few downvotes just to say this: forget the term addiction. Just forget it. This is a buzzword from the world of OO, but nothing more.

Let the real problem be solved. Keep in mind that you are solving a problem, and this problem is a specific programming task. Do not make your problem "dependency injection injection".

We will take an example of a registrar, because this will be the main functionality that many programs will want to have, and there are many different types of logs: one that logs to stderr, one that goes into a file, a database, and one that just does nothing . To combine all of them, you want a type:

 type Logger m = String -> m () 

You can also choose fancier type to save a few keystrokes:

 class PrettyPrint a where pretty :: a -> String type Logger m = forall a. (PrettyPrint a) => a -> m () 

Now let's define some registrars using the latter option:

 noLogger :: (Monad m) => Logger m noLogger _ = return () stderrLogger :: (MonadIO m) => Logger m stderrLogger x = liftIO . hPutStrLn stderr $ pretty x fileLogger :: (MonadIO m) => FilePath -> Logger m fileLogger logF x = liftIO . withFile logF AppendMode $ \h -> hPutStrLn h (pretty x) acidLogger :: (MonadIO m) => AcidState MyDB -> Logger m acidLogger db x = update' db . AddLogLine $ pretty x 

You can see how this creates a dependency graph. acidLogger depends on the database connection for the MyDB database MyDB . Passing arguments to functions is the most natural way to express dependencies in a program. After all, a function is simply a value that depends on another value. This is true for action. If your action depends on the registrar, then, of course, this is the function of registrars:

 printFile :: (MonadIO m) => Logger m -> FilePath -> m () printFile log fp = do log ("Printing file: " ++ fp) liftIO (readFile fp >>= putStr) log "Done printing." 

See how easy it is? At some point, it makes you realize how much easier your life will be when you simply forget all the nonsense that the OO taught you.

+83
Jan 15 '13 at 0:53
source share

Use pipes . I will not say that this is idiomatic, because the library is still relatively new, but I think that it definitely solves your problem.

For example, say you want to bind an interface to some database:

 import Control.Proxy -- This is just some pseudo-code. I'm being lazy here type QueryString = String type Result = String query :: QueryString -> IO Result database :: (Proxy p) => QueryString -> Server p QueryString Result IO r database = runIdentityK $ foreverK $ \queryString -> do result <- lift $ query queryString respond result 

Then we model one interface with the database:

 user :: (Proxy p) => () -> Client p QueryString Result IO r user () = forever $ do lift $ putStrLn "Enter a query" queryString <- lift getLine result <- request queryString lift $ putStrLn $ "Result: " ++ result 

You connect them like this:

 runProxy $ database >-> user 

This will then allow the user to interact with the database from the prompt.

Then we can disconnect the database using the mock database:

 mockDatabase :: (Proxy p) => QueryString -> Server p QueryString Result IO r mockDatabase = runIdentityK $ foreverK $ \query -> respond "42" 

Now we can easily disable the database for the layout:

 runProxy $ mockDatabase >-> user 

Or we can disable the database client. For example, if we noticed that a certain client session caused some strange error, we could reproduce it like this:

 reproduce :: (Proxy p) => () -> Client p QueryString Result IO () reproduce () = do request "SELECT * FROM WHATEVER" request "CREATE TABLE BUGGED" request "I DON'T REALLY KNOW SQL" 

... then connect it like this:

 runProxy $ database >-> reproduce 

pipes allows you to split streaming or interactive behavior into modular components so you can mix and match them as you like, which is the essence of dependency injection.

To learn more about pipes , just read the tutorial at Control.Proxy.Tutorial .

+11
Jan 14 '13 at 23:42
source share

To build an answer on ertes, I think the desired signature for printFile is printFile :: (MonadIO m, MonadLogger m) => FilePath -> m () , which I read as "I will print this file. For this I need perform some I / O and some records. "

I am not an expert, but here is my attempt at this solution. I will be grateful for the comments and suggestions on how to improve this.

 {-# LANGUAGE FlexibleInstances #-} module DependencyInjection where import Prelude hiding (log) import Control.Monad.IO.Class import Control.Monad.Identity import System.IO import Control.Monad.State -- |Any function that can turn a string into an action is considered a Logger. type Logger m = String -> m () -- |Logger that does nothing, for testing. noLogger :: (Monad m) => Logger m noLogger _ = return () -- |Logger that prints to STDERR. stderrLogger :: (MonadIO m) => Logger m stderrLogger x = liftIO $ hPutStrLn stderr x -- |Logger that appends messages to a given file. fileLogger :: (MonadIO m) => FilePath -> Logger m fileLogger filePath value = liftIO logToFile where logToFile :: IO () logToFile = withFile filePath AppendMode $ flip hPutStrLn value -- |Programs have to provide a way to the get the logger to use. class (Monad m) => MonadLogger m where getLogger :: m (Logger m) -- |Logs a given string using the logger obtained from the environment. log :: (MonadLogger m) => String -> m () log value = do logger <- getLogger logger value -- |Example function that we want to run in different contexts, like -- skip logging during testing. printFile :: (MonadIO m, MonadLogger m) => FilePath -> m () printFile fp = do log ("Printing file: " ++ fp) liftIO (readFile fp >>= putStr) log "Done printing." -- |Let say this is the real program: it keeps the log file name using StateT. type RealProgram = StateT String IO -- |To get the logger, build the right fileLogger. instance MonadLogger RealProgram where getLogger = do filePath <- get return $ fileLogger filePath -- |And this is how you run printFile "for real". realMain :: IO () realMain = evalStateT (printFile "file-to-print.txt") "log.out" -- |This is a fake program for testing: it will not do any logging. type FakeProgramForTesting = IO -- |Use noLogger. instance MonadLogger FakeProgramForTesting where getLogger = return noLogger -- |The program doesn't do any logging, but still does IO. fakeMain :: IO () fakeMain = printFile "file-to-print.txt" 
+4
May 26 '14 at 14:32
source share

Another option is to use existentially quantified data types . Take XMonad as an example. There is an interface ( frobby ) for layouts - LayoutClass typeclass:

 -- | Every layout must be an instance of 'LayoutClass', which defines -- the basic layout operations along with a sensible default for each. -- -- ... -- class Show (layout a) => LayoutClass layout a where ... 

and the existential data type of Layout :

 -- | An existential type that can hold any object that is in 'Read' -- and 'LayoutClass'. data Layout a = forall l. (LayoutClass la, Read (la)) => Layout (la) 

which can wrap any ( foo or bar ) instance of the LayoutClass interface. This is the layout itself:

 instance LayoutClass Layout Window where runLayout (Workspace i (Layout l) ms) r = fmap (fmap Layout) `fmap` runLayout (Workspace il ms) r doLayout (Layout l) rs = fmap (fmap Layout) `fmap` doLayout lrs emptyLayout (Layout l) r = fmap (fmap Layout) `fmap` emptyLayout lr handleMessage (Layout l) = fmap (fmap Layout) . handleMessage l description (Layout l) = description l 

Now you can use the Layout data type in the general case only with the methods of the LayoutClass interface. The corresponding layout that implements the LayoutClass interface will be selected at runtime; there are a bunch of them in XMonad.Layout and in xmonad-contrib . And, of course, you can dynamically switch between different layouts:

 -- | Set the layout of the currently viewed workspace setLayout :: Layout Window -> X () setLayout l = do ss@(W.StackSet { W.current = c@(W.Screen { W.workspace = ws })}) <- gets windowset handleMessage (W.layout ws) (SomeMessage ReleaseResources) windows $ const $ ss {W.current = c { W.workspace = ws { W.layout = l } } } 
+3
Jan 15 '13 at 17:16
source share

Enabling dependencies or resolving dependencies is a way of deciding on an implementation as an argument to a function. I insert dependencies on most days of the week on my job in C #.

Named implementations

A strategy template can be implemented using named implementations as follows.

List the names of the implementations:

 data Language = French | Icelandic deriving (Read) 

define a function with different implementations for different names:

 newtype Greeting = Greeting String translateGreetingTo French = Greeting "Bonjour, Monde!" translateGreetingTo Icelandic = Greeting "Halló heimur!" 

if you have a user

 type User = User { _language :: Language } 

you can greet her using

 greet (User (Language language)) = let (Greeting greeting) = (translateGreetingTo language) in (printStrLn greeting) 

Thus, greet will automatically support the new Language when implementing translateGreetingTo. *

Default implementation

When programming dependencies are usually allowed to accept standard implementations. During testing, most of the dependencies should be replaced with simple stub implementations.

To achieve simple default settings for programming, while maintaining a flexible choice for testing, define a default implementation by passing reasonable default dependencies:

 defaultGreeting = translateGreetingTo Icelandic 

Repeat for each function with dependencies (just like in StructureMap, register each interface that you define with the implementation):

 utter :: Greeting -> IO () utter (Greeting greeting) = printStrLn greeting defaultUtter = utter defaultGreeting 

If now we create a more flexible version of the greeting depending on utter (instead of hardcoding printStrLn):

 flexibleGreet :: (Greeting -> IO ()) -> User -> IO () flexibleGreet utterer (User (Language language)) = utterer (translateGreetingToLanguage language) 

Then again we can refator greet like:

 greet = flexibleGreet utter 

If you write more tests than program code, you can save the short name of the flexible (dependent) functions. Conversely, if you write more program codes than tests, you might want to keep the default version names short. Be sure to use systematic names where possible.




* In C #, a strategy template can be similarly implemented using a named implementation:

 interface IGreeting { public string Text; } [... dependency registration and definition of User ...] public class A { public void Greet(User user) { var greeting = serviceLocator.GetNamedInstance<IGreeting>(user.Language); WriteLine(greeting.Text); }} 
0
May 9 '17 at 23:17
source share



All Articles