Haskell中的dependency injection:通俗地解决任务

什么是dependency injection的惯用Haskell解决scheme?

例如,假设你有一个接口frobby ,并且你需要传递一个符合frobby的实例(可能有这些实例的多个变体,比如foobar )。

典型的操作是:

  • 函数需要一些值X并返回一些值Y 例如,这可能是一个数据库访问器,采取一个SQL查询和连接器,并返回一个数据集。 你可能需要实现postgres,mysql和一个模拟testing系统。

  • 函数需要一些Z值,并返回一个关于Z的闭包,专门用于在运行时select的给定的foobar样式。

一个人解决了这个问题如下:

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

但是我不知道这是否是pipe理这个任务的标准方法。

我认为这里的正确答案是,我可能会收到一些简单的说法:忘记术语dependency injection 。 把它忘了吧。 这是来自OO世界的一个stream行的stream行词,但仅此而已。

让我们解决真正的问题。 请记住,你正在解决一个问题,这个问题是手头的特定编程任务。 不要让你的问题“实施dependency injection”。

我们将以logging器为例,因为这是许多程序所需要的基本function,并且有许多不同types的logging器:一个logging到stderr,一个logging到文件,一个数据库,一个什么都不做 要统一所有你想要的types:

 type Logger m = String -> m () 

您还可以select一个发烧友types来保存一些按键:

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

现在让我们定义一些使用后一种变体的logging器:

 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 

你可以看到这是如何构build一个依赖关系图。 acidLogger依赖于MyDB数据库布局的数据库连接。 将parameter passing给函数是关于在程序中expression依赖关系的最自然的方式。 毕竟函数只是一个依赖于另一个值的值。 行动也是如此。 如果你的行为取决于logging器,那么自然就是logging器的function:

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

看看这是多么容易? 在某个时候,这让你意识到你的生活将会变得多么容易,当你忘记了OO教给你的所有的废话时。

使用pipes 。 我不会说这是惯用的,因为图书馆还是比较新的,但是我认为它完全可以解决你的问题。

例如,假设您想将一个接口包装到某个数据库中:

 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 

然后,我们可以build模一个接口到数据库:

 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 

你像这样连接它们:

 runProxy $ database >-> user 

这将允许用户从提示符与数据库进行交互。

然后我们可以用模拟数据库来切换数据库:

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

现在我们可以非常容易地将模拟数据库切换出来:

 runProxy $ mockDatabase >-> user 

或者我们可以切换出数据库客户端。 例如,如果我们注意到特定的客户端会话触发了一些奇怪的错误,我们可以像这样重现它:

 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" 

…然后像这样挂钩:

 runProxy $ database >-> reproduce 

pipes可以将stream模式或交互式行为分解为模块化组件,因此您可以根据需要混合匹配它们,这是dependency injection的本质。

要了解有关pipes更多信息,请阅读Control.Proxy.Tutorial上的教程。

为了构buildertes的答案,我认为printFile所需的签名是printFile :: (MonadIO m, MonadLogger m) => FilePath -> m () ,我读为“我将打印给定的文件。需要做一些IO和一些日志logging。“

我不是专家,但这是我在这个解决scheme的尝试。 对于如何改善这一点,我将不胜感激。

 {-# 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's 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" 

另一种select是使用存在量化的数据types 。 以XMonad为例。 有一个布局的( frobby )接口 – LayoutClasstypes类:

 -- | 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 ... 

和存在的数据types布局 :

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

可以包装任何( foobar )的LayoutClass接口实例。 这本身就是一个布局:

 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 

现在可以只使用LayoutClass接口方法来使用Layout数据types。 实现LayoutClass接口的适当布局将在运行时被选中,在XMonad.Layout和xmonad-contrib中有一堆。 当然,可以dynamic切换不同的布局:

 -- | 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 } } } 

dependency injection或依赖关系parsing是决定将哪个实现作为参数提供给函数的工具。 我在C#作业的大部分日子里注入了依赖项。

命名实现

策略模式可以使用命名实现来实现,如下所示。

列举实现的名称:

 data Language = French | Icelandic deriving (Read) 

使用不同的实现为不同的名称定义一个函数:

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

如果你现在有一个用户

 type User = User { _language :: Language } 

你可以问候她使用

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

这样,当通过translateGreetingTo实现时, greet将自动支持新Language

默认实现

在编程时,依赖关系通常可以被认为是默认的实现。 在testing时,大多数依赖项应该换成简单的存根实现。

为了实现简单的编程默认,同时保持灵活的testingselect,通过传递合理的默认依赖关系来定义默认实现:

 defaultGreeting = translateGreetingTo Icelandic 

对每个具有依赖关系的函数重复一次(就像在StructureMap中那样,注册您使用实现定义的每个接口):

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

如果我们现在根据utter(而不是硬编码printStrLn)创build一个更灵活的问候语版本:

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

然后,再次,我们可以作为:

 greet = flexibleGreet utter 

如果编写比程序代码更多的testing,则可能希望将灵活(依赖)函数的名称缩短。 相反,如果您编写比testing更多的程序代码,您可能希望将缺省实现的名称缩短。 确保在可能的情况下使用系统名称。


*在C#中,策略模式可以类似的使用命名实现来实现:

 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); }}