3

我正在编写一些代码来登录 Haskell。在命令式语言中,我会(已经)写过类似的东西:

log = new Logger();
log.registerEndpoint(new ConsoleEndpoint(settings));
log.registerEndpoint(new FileEndpoint(...));
log.registerEndpoint(new ScribeEndpoint(...));
...
log.warn("beware!")
log.info("hello world");

甚至可能制作log一个全局静态,所以我不必传递它。实际的端点和设置可能会在启动时从配置文件中配置,例如。一种用于生产,一种用于开发。

在 Haskell 中做这样的事情有什么好的模式?

4

3 回答 3

6

pipes包允许您将数据生成与数据消费分开。您将程序编写为 logString的生产者,然后在运行时选择如何使用这些Strings。

例如,假设您有以下简单程序:

import Control.Proxy

program :: (Proxy p) => () -> Producer p String IO r
program () = runIdentityP $ forever $ do
    lift $ putStrLn "Enter a string:"
    str <- lift getLine
    respond $ "User entered: " ++ str

该类型表示它是一个Producerof Strings(在本例中为日志字符串),它也可以IO使用lift. IO因此,对于不涉及日志记录的普通命令,您只需使用lift. 每当您需要记录某些内容时,您都可以使用该respond命令,该命令会生成一个String.

这会创建一个抽象的字符串生产者,它没有指定如何使用它们。这让我们可以推迟选择如何使用生成String的 s。每当我们调用该respond命令时,我们都会抽象地将我们的日志字符串交给一些尚未指定的下游阶段,该阶段将为我们处理它。

现在让我们编写一个程序,该程序Bool从命令行获取一个标志,该标志指定是否将输出写入stdout或写入文件"my.log"

import System.IO
import Options.Applicative

options :: Parser Bool
options = switch (long "file")

main = do
    useFile <- execParser $ info (helper <*> options) fullDesc
    if useFile
        then do
            withFile "my.log" WriteMode $ \h ->
                runProxy $ program >-> hPutStrLnD h
        else runProxy $ program >-> putStrLnD

如果用户在命令行中没有提供任何标志,则useFile默认为False,表示我们要登录到stdout。如果用户提供--file标志,则useFile默认为True,表示我们要登录到"my.log"

现在,检查两个if分支。第一个分支使用运算符String将​​生成的 s馈送program到文件中(>->)。可以将其视为接受 a并创建 s 的抽象消费者的hPutStrLnD东西,该消费者将每个字符串写入该句柄。当我们连接到时,我们将每个日志字符串发送到文件:HandleStringprogramhPutStrLnD

$ ./log
Enter a string:
Test<Enter>
User entered: Test
Enter a string:
Apple<Enter>
User entered: Apple
^C
$

第二个if分支将Strings 馈送到putStrLnD,它只是将它们写入stdout

$ ./log --file
Enter a string:
Test<Enter>
Enter a string:
Apple<Enter>
^C
$ cat my.log
User entered: Test
User entered: Apple
$

尽管从生产中解耦生成,pipes仍然立即流式传输所有内容,因此输出阶段(即hPutStrLnDputStrLnD)将在生成后立即写出,Strings并且不会缓冲Strings 或等待程序完成。

请注意,通过将生成与实际日志记录操作分离,我们获得了在最后时刻String注入消费者依赖项的能力。String

要了解有关如何使用pipes更多信息,我建议您阅读教程pipes

于 2013-02-05T23:34:06.147 回答
5

如果您只有一组固定的端点,这是一种可能的设计:

data Logger = Logger [LoggingEndpoint]
data LoggingEndpoint = ConsoleEndpoint ... | FileEndpoint ... | ScribeEndpoint ... | ...

那么实现这个应该很简单:

logWarn :: Logger -> String -> IO ()
logWarn (Logger endpoints) message = forM_ logToEndpoint endpoints
  where
    logToEndpoint :: LoggingEndpoint -> IO ()
    logToEndpoint (ConsoleEndpoint ...) = ...
    logToEndpoint (FileEndpoint ...) = ...

如果您想要可扩展的端点集,有一些方法可以做到,其中最简单的方法是定义LoggingEndpoint为函数记录,基本上是一个 vtable:

data LoggingEndpoint = LoggingEndpoint { 
    logMessage :: String -> IO (),
    ... other methods as needed ...
}

consoleEndpoint :: Settings -> LoggingEndpoint
consoleEndpoint (...) = LoggingEndpoint { 
    logMessage = \message -> ...
    ... etc ...
}

然后,logToEndpoint简单地变成

logToEndpoint ep = logMessage ep message
于 2013-02-05T22:43:30.607 回答
2

在 Real World Haskell 中,他们描述了如何使用 Writer monad 来做到这一点,比我能解释的要好得多:http: //book.realworldhaskell.org/read/programming-with-monads.html#id649416

另请参阅有关 monad 转换器的章节:http: //book.realworldhaskell.org/read/monad-transformers.html

于 2013-02-05T22:42:59.913 回答