I have been looking at logging options and settled on NLog and logging to a database
I am fairly new to functional programming and have come from a background in C# OOP.
How would I implement logging in a functional way in F#?
Do I
I want to avoid using a commercial logging option just because my projects are quite small.
Thanks for your time.
Access the logger through a static method
This approach is fine for experimental applications, but generally speaking it is the instance of Service Locator which is commonly considered as antipattern.
More solid alternative is dependency injection (DI). Since F# is hybrid language, you can use OOP concepts and apply the DI pattern in common way for OOP. However if you want to use the functional style, there is no single common functional pattern for DI in F#.
Create the logger at the top level and just pass it in to every function
This is just the most straightforward way of using DI in functional code. This approach may work fine for small applications, however if along with logging you have other cross-cutting concerns (such as configuration), you may end up with explosion of arguments, which clutters up the code. There are different approaches how to address this problem. I have settled with approach described here.
You can apply this approach in a following way.
Your function with business logic may look like this:
let doSomething env =
let logger = getLogger env
logger.Debug("Do something")
// Do something
It means that logger
reference is provided via env
parameter and accessed via the function getLogger
.
This code should reference the common logging module, which may look like this:
[<Interface>]
type ILoggerProvider =
abstract Logger: ILogger
let getLogger (env: #ILoggerProvider) = env.Logger
Notice that type of env
parameter of doSomething
is automatically inferred as #ILoggerProvider
(that is inherits ILoggerProvider
).
Imaging that later in the same business logic we need to access the configuration (along with logging). In this case we do not need new parameter, but access the configuration from the same env
parameter in similar way: let configuration = getConfiguration env
, provided that common module for configuration is created as well:
[<Interface>]
type IConfigurationProvider =
abstract Configuration: IConfiguration
let getConfiguration (env: #IConfigurationProvider) = env.Configuration
In this case type of env
parameter of doSomething
is automatically re-inferred to
'a (requires 'a :> ILoggerProvider and 'a :> IConfigurationProvider)
which means that it should inherit both ILoggerProvider
and IConfigurationProvider
.
On application level you need to create the instance of environment and pass it to business logic in order to provide the access to logging and configuration (and possibly other services). You can do it in a following way:
[<Interface>]
type IEnvironment =
inherit ILoggerProvider
inherit IConfigurationProvider
let createEnvironment (logger, configuration) =
{ new IEnvironment with
member self.Logger = logger
member self.Configuration = configuration }
let createLogger () = // create logger...
let createConfiguration () = // create configuration...
let logger = createLogger ()
let configuration = createConfiguration ()
let env = createEnvironment (logger, configuration)
// Call business logic
doSomething env
Notice, that it is easy to extend this code with new cross-cutting concerns, since each of business logic functions is aware only of used services (such as logging, configuration) and knows nothing about IEnvironment
interface and all its content.
You can find the full version of my example here.
More comprehensive overview of DI approaches in F# you may find here.