依赖注入的惯用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但我不知道这是不是管理这个任务的规范方法。
我认为正确的答案是:忘记依赖注入这个术语。算了吧。这是一个来自OO世界的时髦流行语,但仅此而已。
让我们解决真正的问题。请记住,您正在解决一个问题,而这个问题就是手头的特定编程任务。不要让你的问题"实现依赖注入"。
我们将以日志记录器为例,因为这是许多程序都想拥有的基本功能,并且有许多不同类型的日志记录器:一种记录到标准错误,一种记录到文件,数据库,还有一种什么都不做。要统一它们,你需要一个类型:
type Logger m = String -> m ()
你也可以选择一个更花哨的类型来保存一些按键:
class PrettyPrint a where
pretty :: a -> String
type Logger m = forall a. (PrettyPrint a) => a -> m ()
现在让我们使用后一种变体定义几个记录器:
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
你可以看到这是如何构建一个依赖关系图的。acidLogger
依赖于MyDB
数据库布局的数据库连接。向函数传递参数是表达程序中依赖关系的最自然的方式。毕竟,函数只是一个依赖于另一个值的值。对于行动也是如此。如果您的操作依赖于记录器,那么它自然是记录器的函数:
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
然后我们可以建模一个接口到数据库:
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
允许你将流或交互行为拆分为模块化组件,这样你就可以随心所欲地混合和匹配它们,这就是依赖注入的本质。
要了解更多关于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"
另一个选择是使用存在量化的数据类型。让我们以XMonad为例。有一个(frobby
)接口用于布局- 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
...
和存在数据类型Layout:
-- | An existential type that can hold any object that is in 'Read'
-- and 'LayoutClass'.
data Layout a = forall l. (LayoutClass l a, Read (l a)) => Layout (l a)
可以封装LayoutClass
接口的任何(foo
或bar
)实例。它本身就是一个布局:
instance LayoutClass Layout Window where
runLayout (Workspace i (Layout l) ms) r = fmap (fmap Layout) `fmap` runLayout (Workspace i l ms) r
doLayout (Layout l) r s = fmap (fmap Layout) `fmap` doLayout l r s
emptyLayout (Layout l) r = fmap (fmap Layout) `fmap` emptyLayout l r
handleMessage (Layout l) = fmap (fmap Layout) . handleMessage l
description (Layout l) = description l
现在可以只对LayoutClass
接口方法使用Layout
数据类型。适当的布局,实现LayoutClass
接口将在运行时选择,有一堆在XMonad。布局和在xmonad-contrib。当然,也可以在不同的布局之间动态切换:
-- | 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 } } }