I'm gradually switching into F# for a lot of my home projects but I'm a little stumped as to how to wire together complete applications, and more particularly cross-cutting concerns.
In C# if I want to log stuff I'd use dependency injection to pass an ILogger into each class, and then this can be called nice and easily from the code. I can verify in my tests that given a particular situation logs are written, by passing in a mock and verifying it.
public class MyClass
{
readonly ILogger _logger;
public MyClass(ILogger logger)
{
_logger = logger;
}
public int Divide(int x, int y)
{
if(y == 0)
{
_logger.Warn("y was 0");
return 0;
}
return x / y;
}
}
In F# I'm using modules a lot more, so the above would become
module Stuff
let divde x y =
match y with
| 0 -> 0
| _ -> x / y
Now if I had a module called Logging I could just open that and use a log function from there in the case of y being 0, but how would I inject this for unit testing?
I could have each function take a log function (string -> unit) and then use partial application to wire them up but that seems like an awful lot of work, as would creating a new function that wraps the actual call inside a logging call. Is there a particular pattern or a bit of F# that I'm missing that can do it? (I've seen the kprintf function but I still don't know how you'd specify the function for various test scenarios, whilst using a concrete implementation for the complete application)
Similarly how would you stub out a repository that fetched data? Do you need to have some class instantiated and set the CRUD functions on it, or is there a way to inject which modules you open (aside from #define)
This is a basic answer. Firstly, it seems you're thinking of classes and modules as being interchangeable. Classes encapsulate data, and in that sense are more analogous to records and DUs. Modules, on the other hand, encapsulate functionality (they're compiled to static classes). So, I think you already mentioned your options: partial function application, passing the functions around as data, or... dependency injection. For your particular case it seems easiest to keep what you have.
An alternative is using preprocessor directives to include different modules.
#if TESTING
open LogA
#else
open LogB
#endif
DI isn't necessarily a poor fit in a functional language. It's worth pointing out, F# makes defining and fulfilling interfaces even easier than, say, C#.
If you won't need to change the logger at runtime, then using a compiler directive or #if
to choose between two implementations of a logger (as suggested by Daniel) is probably the best and the simplest way to go.
From the functional point of view, dependency injection means the same thing as parameterizing all your code by a logging function. The bad thing is that you need to propagate the function everywhere (and that makes the code a bit messy). You can also just make a global mutable variable in a module and set it to an instance of some ILogger
interface - I think this is actually quite acceptable solution for F#, because you need to change this variable only in a couple of places.
Another (more "pure") alternative would be to define a workflow (aka monad) for logging. This is a good choice only if you write all your code in F#. This example is discussed in Chapter 12 of my book, which is available as a free sample. Then you can write something like this:
let write(s) = log {
do! logMessage("writing: " + s)
Console.Write(s) }
let read() = log {
do! logMessage("reading")
return Console.ReadLine() }
let testIt() = log {
do! logMessage("starting")
do! write("Enter name: ")
let! name = read()
return "Hello " + name + "!" }
The good thing about this approach is that it is a nice functional technique. Testing code should be easy, because a function that returns Log<'TResult>
essenitally gives you a value as well as a record of its side-effects, so you can just compare the results! However, it may be an overkill, because you'd have to wrap every computation that uses logging in the log { .. }
block.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With