什么是依赖注入的惯用Haskell解决方案?
例如,假设您有一个接口frobby
,并且您需要传递一个符合frobby
周围的实例(可能有多种类型的这些实例,比方说foo
,和bar
).
典型的操作是:
获取一些值X
并返回一些值的函数Y
.例如,这可能是一个数据库访问器,采用SQL查询和连接器并返回数据集.您可能需要实现postgres,mysql和模拟测试系统.
获取某些值Z
并返回与运行时选择Z
的特定foo
或bar
样式相关的闭包的函数.
一个人解决了这个问题如下:
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教给你的所有废话时,这会让你意识到你的生活会变得多么容易.
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中的教程.
为了建立ertes的答案,我认为所需的签名printFile
是printFile :: (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)