3

I'm writing some code to do logging in Haskell. In imperative languages I would (have) written something like:

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

Maybe even make log a global static so I don't have to pass it around. The actual endpoints and setting would be configured at startup from a config file maybe, eg. one for production, one for development.

What's a good pattern to do something like this in Haskell?

4

3 に答える 3

6

このpipesパッケージを使用すると、データ生成とデータ消費を分離できます。プログラムをログStringのプロデューサーとして作成し、実行時にそれらのログを使用する方法を選択しますString

たとえば、次の簡単なプログラムがあるとします。

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

タイプは、それがProducers String(この場合はログ文字列)であり、をIO使用してコマンドを呼び出すこともできることを示していますlift。したがってIO、ロギングを伴わない通常のコマンドの場合は、を使用するだけliftです。何かをログに記録する必要があるときはいつでも、respondコマンドを使用して。を生成しStringます。

これにより、文字列の消費方法を指定しない文字列の抽象的なプロデューサーが作成されます。これにより、生成されたを後で使用する方法の選択を延期することができますString。コマンドを呼び出すときはいつでも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次に、2つのブランチを確認します。最初のブランチは、演算子を使用して、生成するStringsをprogramファイルにフィードします。を取り、各文字列をそのハンドルに書き込むsの抽象的なコンシューマーを作成するものと(>->)考えてください。に接続すると、すべてのログ文字列がファイルに送信されます。hPutStrLnDHandleStringprogramhPutStrLnD

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

2番目のブランチはsをにifフィードします。StringputStrLnDstdout

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

生成を本番から切り離しpipesても、すべてをすぐにストリーミングするため、出力ステージ(つまりhPutStrLnD、 )は生成された直後にputStrLnD書き込みを行い、 sをバッファリングしたり、プログラムが終了するまで待機したりしません。StringsString

生成を実際のロギングアクションから切り離すことにより、最後の瞬間にコンシューマーの依存関係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モナドを使用してこれを行う方法を説明しています。これは、私が説明できるよりもはるかに優れています。http: //book.realworldhaskell.org/read/programming-with-monads.html#id649416

モナド変換子に関する章も参照してください:http://book.realworldhaskell.org/read/monad-transformers.html

于 2013-02-05T22:42:59.913 に答える