Haskell中的依赖注入:习惯性地解决任务

Pau*_*han 53 haskell

什么是依赖注入的惯用Haskell解决方案?

例如,假设您有一个接口frobby,并且您需要传递一个符合frobby周围的实例(可能有多种类型的这些实例,比方说foo,和bar).

典型的操作是:

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

  • 获取某些值Z并返回与运行时选择Z的特定foobar样式相关的闭包的函数.

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

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

但我不知道这是否是规范管理此任务的方法.

ert*_*tes 108

我认为这里的正确答案是,我可能只会因为这样说而收到一些downvotes:忘记依赖注入一词.把它忘了吧.这是OO世界的时尚流行语,但仅此而已.

让我们解决真正的问题.请记住,您正在解决问题,而该问题是手头的特定编程任务.不要让你的问题"实现依赖注入".

我们将以记录器为例,因为这是许多程序想要拥有的基本功能,并且有许多不同类型的记录器:一个记录到stderr,一个记录到文件,一个数据库,而且什么都不做.要统一所有这些,你想要一个类型:

type Logger m = String -> m ()
Run Code Online (Sandbox Code Playgroud)

你也可以选择一个发烧友类型来保存一些击键:

class PrettyPrint a where
    pretty :: a -> String

type Logger m = forall a. (PrettyPrint a) => a -> m ()
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

您可以看到这是如何构建依赖关系图的.这acidLogger取决于MyDB数据库布局的数据库连接.将参数传递给函数是在程序中表达依赖关系的最自然的方式.毕竟函数只是一个依赖于另一个值的值.行动也是如此.如果您的操作依赖于记录器,那么它自然就是记录器的功能:

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

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

  • 你还会收到更多赞成这个... (22认同)
  • +1到@panurg.这错过了DI的基本元素,它实际上是自动注入依赖项,而不仅仅是允许插入依赖项,这本身就是必需的,但还不够.如果该帖子本身并没有背叛对为什么OO社区如DI的缺乏理解,那么作者对OO最后的轻率评论会更加尖锐. (15认同)
  • 明白我使用"DI"作为一个特别方便的概念的简写,我不知道如何在没有很多单词的情况下引用它.你将它作为一个特定类型别名的Haskell的弱知识来构建它,并利用该类型推断来确保调用函数能够正确运行函数.当然,它并不是特别神奇(也不是OO变体).我希望(ed)掌握针对特定问题的完全非OO解决方案. (8认同)
  • 没有得到它,你没有解释最重要的部分,那就是应该将日志发送到printFile,因为这种胶水代码是DI的一个原因. (4认同)
  • @Paul:我的观点是你需要了解OO解决方案究竟解决了什么问题才能以非OO方式解决它.在这种情况下,你会发现`ReaderT`非常接近DI的作用.这只是函数和函数应用程序,在某些位置隐藏应用程序.换句话说,在大多数情况下,OO只是为已知和旧的坏想法发明了新的术语,并且仅仅因为人们似乎过于兴奋,即使基本概念实际上是一个非常糟糕的想法,如单身类(全局)伪装的可变变量).最好忘记OO. (2认同)
  • 如果您有很多依赖项,您可能希望将它们包装在 State 或 Reader monad 之类的东西中。您还可以嵌套 monad。 (2认同)
  • 提供的解决方案对于 Haskell 或其他函数式编程语言来说看起来不错。然而,它很少是 OOP 中最好的。它看起来更像是穷人的 DI,随着复杂性而变得越来越乏味。存在用于构建对象图(IOC 容器)的自动化工具是有原因的。在负面语境中提及面向对象并不是一个好主意。您会看到,面向对象有一些优点,如果使用得当,可以带来很好的解决方案。即使是单例也有其有效的用途。您认为内存缓存是什么?我的观点是,并非所有事情都完全适合功能性的做事方式。 (2认同)
  • @Vakhtang OOP 没有围绕参数化提供足够强大的人体工程学或安全保证,因此这是站得住脚的,是的。在 OO 中这将是一个糟糕的主意,但话又说回来,使用元编程在运行时动态注入依赖项在 Haskell 中将是一个灾难性的坏主意。 (2认同)

Gab*_*lez 12

使用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
Run Code Online (Sandbox Code Playgroud)

然后我们可以为数据库建模一个接口:

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
Run Code Online (Sandbox Code Playgroud)

你这样连接它们:

runProxy $ database >-> user
Run Code Online (Sandbox Code Playgroud)

然后,这将允许用户从提示中与数据库交互.

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

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

现在我们可以非常轻松地为模拟数据库切换出数据库:

runProxy $ mockDatabase >-> user
Run Code Online (Sandbox Code Playgroud)

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

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"
Run Code Online (Sandbox Code Playgroud)

...然后把它连接起来:

runProxy $ database >-> reproduce
Run Code Online (Sandbox Code Playgroud)

pipes 允许您将流式或交互式行为拆分为模块化组件,以便您可以随意混合和匹配它们,这是依赖注入的本质.

要了解更多信息pipes,请阅读Control.Proxy.Tutorial中的教程.


Mic*_*ski 5

为了建立ertes的答案,我认为所需的签名printFileprintFile :: (MonadIO m, MonadLogger m) => FilePath -> m (),我读为“我将打印给定的文件。为此,我需要做一些IO和一些日志记录。”

我不是专家,但这是我尝试此解决方案的尝试。我将非常感谢您提出有关如何改进此问题的意见和建议。

{-# 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"
Run Code Online (Sandbox Code Playgroud)