4

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 ...
ebb
  • 9,297
  • 18
  • 72
  • 123

2 Answers2

5

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.

Community
  • 1
  • 1
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • 4
    Also see "Railway oriented programming" from FsharForFunAndProfit.com (http://fsharpforfunandprofit.com/posts/recipe-part2/) for a general implementation pattern. – Huusom Jul 15 '14 at 12:23
  • Thanks for your comprehensive answer :-) - Would the module `XyzLog` correspond to `MongoDbLog`, and then when logging functions from ex. module `Web` the logging module would be `WebLog` - or would you simply throw all functions that needs to be logged, into the same logging module? – ebb Jul 16 '14 at 18:37
  • @ebb `XyzLog` could be a logging module based on e.g. Log4Net, in which case a natural name for it would be `Log4NetLog`, etc. The point of this design is that the `MongoDb` module and `XyzLog` are completely decoupled, so there's no reason to include the name *Mongo* in the name of the logging module. – Mark Seemann Jul 17 '14 at 08:09
  • I see. Why not just wrap the logging framework into a interface, and have the logging framework take the interface as an argument? - That way you only ever need one logging module? – ebb Jul 17 '14 at 09:11
  • `let tryGetServer f (logger : ILogger) connectionString = ...` - Note that the logger is of type `ILogger`. That would make it possible to pass in any logger that implements the `ILogger` interface. - And there would be no need for a new logging module for each logging framework? – ebb Jul 17 '14 at 10:17
  • @ebb OK, that may make sense - it depends on how complex you plan on making `ILogger`, but if you plan on making `ILogger` define several members, it may be a good solution. – Mark Seemann Jul 17 '14 at 12:38
4

There is a more general way to implement cross-cutting concerns such as logging with a functional language. The example I have is from an async service library (think ASP.NET MVC and ActionFilters) but the same applies here as well. As stated by Mark, the function tryGetServer is of type string -> MongoServer option. Suppose we abstract it to:

type Service<'a, 'b> = 'a -> 'b option

Then suppose we also have a type as follows:

type Filter<'a, 'b> = 'a -> Service<'a, 'b> -> 'b option

A filter is a function which takes a value 'a and a Service<'a, 'b> and then returns a value of the same type as the Service<'a, 'b> function. The simplest filter is a function which simply passes the 'a it receives directly to the service and returns the value it gets from the service. A more interesting filter would be a function which prints a log message after receiving output from the service.

let loggingFilter (connStr:string) (tryGetServer:string -> MongoServer option) : Filter<string, MongoServer option> =
  let server = tryGetServer connStr 
  match tryGetServer connStr with
  | Some _ ->
    logger.Information "Successfully connected to the database server."
    server
  | None -> 
    logger.Information "Unable to connect to the database server."
    server

Then if you have the following defined:

type Continuation<'a,'r> = ('a  -> 'r) -> 'r

module Continuation =     

    let bind (m:Continuation<'a, 'r>) k c = m (fun a -> k a c)

module Filter =

    /// Composes two filters into one which calls the first one, then the second one.
    let andThen (f2:Filter<_,,_>) (f1:Filter<_,_>) : Filter<_,_> = fun input -> Continuation.bind (f1 input) f2

    /// Applies a filter to a service returning a filtered service.
    let apply (service:Service<_,_>) (filter:Filter<_,_>) : Service<_,_> = fun input -> filter input service

    /// The identity filter which passes the input directly to the service and propagates the output.
    let identity : Filter<_,_> = fun (input:'Input) (service:Service<_,_>) -> service input

You can apply a filter to a service and get back the original service type but which now does logging:

let tryGetServerLogable = Filter.apply tryGetServer loggingFilter

Why bother? Well, now you can compose filters together. For example you may add a filter which measures the time it takes to create a connection and you can then combine them using Filter.andThen. The gist I originally made is here.

Another approach to consider is the use of a writer monad. With the writer monad, you can defer the actual printing of log messages until some well defined point, but still have similar composition characteristics.

eulerfx
  • 36,769
  • 7
  • 61
  • 83