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 func
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 =
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.