Log in function or function using it?

f#

Solution

Approach 2 is better. In general, logging is a Cross-Cutting Concern, so it's best decoupled from implementation details. Cross-Cutting Concerns are best addressed via Composition; in OOD, this can be done with Decorators or Interceptors. In FP, we can sometimes learn from OOD, because many of the principles translate from objects to closures.

However, instead of using Approach 2 above verbatim, I'd rather prefer something like this:

module MongoDb =
    let tryGetServer connectionString =
        try
            let server = MongoClient(connectionString).GetServer()
            server.Ping()
            Some server
        with _ -> None

Notice that the `MongoDb` module has no knowledge of logging. This follows the Single Responsibility Principle, which is also valuable in Functional Programming.

The `tryGetServer` function has this signature:

string -> MongoServer option

Now you can define a logging function, totally decoupled from the `MongoDb` module:

module XyzLog =
    type Logger() =
        member this.Information message = ()

    let tryGetServer f (logger : Logger) connectionString  =
        match f connectionString with
        | None ->
            logger.Information "Unable to connect to the database server."
            None
        | Some srv ->
            logger.Information "Successfully connected to the database server."
            Some srv

Here, you can imagine that `XyzLog` is a placeholder for a particular logging module, utilising Serilog, Log4Net, NLog, your own custom logging framework, or similar...

The `f` argument is a function with the generic signature `'a -> 'b option`, of which `MongoDb.tryGetServer` is a specialization.

This means that you can now define a partially applied function like this:

let tgs = XyzLog.tryGetServer MongoDb.tryGetServer (XyzLog.Logger())

The function `tgs` also has the signature

string -> MongoServer option

So any client that depends on a function with this signature can use `MongoDb.tryGetServer` or `tgs` interchangeably, without knowing the difference.

This enables you to change you mind or refactor both `MongoDb.tryGetServer` and your logging infrastructure independently of each other.

Problem

Is it best (I'm aware of that there's no silver bullet, but there may be some advantage by using one over the other) - to log in the calling function, or the function calling it? Examples: Approach 1 ``` module MongoDb = let tryGetServer connectionString = try let server = new MongoClient(connectionString).GetServer() server.Ping() Some server with _ -> None ``` Usage: ``` match MongoDb.tryGetServer Config.connectionString with | None -> logger.Information "Unable to connect to the database server." // ... code ... | Some srv -> logger.Information "Successfully connected to the database server." // ... code ... ``` Approach 2 ``` module MongoDb = let tryGetServer connectionString = try let server = new MongoClient(connectionString).GetServer() server.Ping() Some server with _ -> None let tryGetServerLogable connectionString logger = match tryGetServer connectionString with | None -> logger.Information "Unable to connect to the database server." None | Some srv -> logger.Information "Successfully connected to the database server." Some srv ``` Usage: ``` match MongoDb.tryGetServerLogable Config.connectionString logger with | None -> // ... code ... | Some srv -> // ... code ... ```

Original source

Related problems