在Haskell中记录模式

Mar*_*eni 3 haskell

我正在编写一些代码来记录Haskell.在命令式语言中,我会写(例如):

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

也许甚至可以创建log一个全局静态,所以我不必传递它.实际端点和设置将在启动时从配置文件配置,例如.一个用于生产,一个用于开发.

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

Gab*_*lez 6

pipes软件包允许您将数据生成与数据消耗分开.您将程序编写为log String的生成器,然后在运行时选择如何使用这些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
Run Code Online (Sandbox Code Playgroud)

类型说,这是一个ProducerStringS(在这种情况下,登录字符串),也可以调用IO使用的命令lift.因此IO,对于不涉及日志记录的普通命令,您只需使用lift.每当你需要记录某些内容时,你都会使用respond命令生成一个String.

这将创建一个字符串的抽象生成器,但不指定它们的使用方式.这让我们可以推迟选择如何使用生成的Strings.每当我们调用该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
Run Code Online (Sandbox Code Playgroud)

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

现在,看看这两个if分支.第一个分支使用运算符Stringprogram生成的s 提供给文件(>->).可以把它想象hPutStrLnD成一个Handle并创建Strings 的抽象消费者,将每个字符串写入该句柄.当我们连接program到时hPutStrLnD,我们将每个日志字符串发送到该文件:

$ ./log
Enter a string:
Test<Enter>
User entered: Test
Enter a string:
Apple<Enter>
User entered: Apple
^C
$
Run Code Online (Sandbox Code Playgroud)

第二个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
$
Run Code Online (Sandbox Code Playgroud)

尽管将生成与生产分离,pipes仍然会立即对所有内容进行流处理,因此输出阶段(即hPutStrLnDputStrLnD)将Strings在生成后立即写出,并且不会缓冲Strings或等到程序完成.

请注意,通过将String生成与实际日志记录操作分离,我们可以String在最后一刻注入使用者依赖关系.

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


hza*_*zap 5

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

data Logger = Logger [LoggingEndpoint]
data LoggingEndpoint = ConsoleEndpoint ... | FileEndpoint ... | ScribeEndpoint ... | ...
Run Code Online (Sandbox Code Playgroud)

然后应该直截了当地实现这个:

logWarn :: Logger -> String -> IO ()
logWarn (Logger endpoints) message = forM_ logToEndpoint endpoints
  where
    logToEndpoint :: LoggingEndpoint -> IO ()
    logToEndpoint (ConsoleEndpoint ...) = ...
    logToEndpoint (FileEndpoint ...) = ...
Run Code Online (Sandbox Code Playgroud)

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

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

consoleEndpoint :: Settings -> LoggingEndpoint
consoleEndpoint (...) = LoggingEndpoint { 
    logMessage = \message -> ...
    ... etc ...
}
Run Code Online (Sandbox Code Playgroud)

然后,logToEndpoint简单地变成

logToEndpoint ep = logMessage ep message
Run Code Online (Sandbox Code Playgroud)