Haskell中的dependency injection:通俗地解决任务
什么是dependency injection的惯用Haskell解决scheme?
例如,假设你有一个接口frobby
,并且你需要传递一个符合frobby
的实例(可能有这些实例的多个变体,比如foo
和bar
)。
典型的操作是:
-
函数需要一些值
X
并返回一些值Y
例如,这可能是一个数据库访问器,采取一个SQL查询和连接器,并返回一个数据集。 你可能需要实现postgres,mysql和一个模拟testing系统。 -
函数需要一些
Z
值,并返回一个关于Z
的闭包,专门用于在运行时select的给定的foo
或bar
样式。
一个人解决了这个问题如下:
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
)接口 – LayoutClass
types类:
-- | 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)
可以包装任何( foo
或bar
)的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); }}