I like the idea of writing pure functions, but I'm having trouble understand ways to combine them that lead to testable code. I'm used to extracting classes and then stubbing appropriately, and feel that I'm missing some key insight into functional programming.
Here is an example that I've slimmed down from a problem I'm currently facing.
I want to take a list of a list of dates and filter it for those which meet an 'opportunity' criteria.
In C# it looks something like:
static List<List<DateTime>> Opportunities(List<List<DateTime>> dates)
{
return dates.Where(ds => HasOpportunity(ds)).ToList();
}
static bool HasOpportunity(List<DateTime> dates)
{
var threshold = 0.05D;
var current = OpportunityProbability(dates, DateTime.Now);
var previous = OpportunityProbability(dates, DateTime.Now.Subtract(TimeSpan.FromDays(30)));
return previous >= threshold && current < threshold;
}
static double OpportunityProbability(List<DateTime> dates, DateTime endDate)
{
// does lots of math
return 0.0D;
}
So at the tip we have OpportunityProbability that I know how to test. The trouble I have is in HasOpportunity and further up the chain (Opportunities).
The only way I know how to test HasOpportunity is by stubbing out OpportunityProbability but I can't do that. And I don't want to create fake data to satisfy the design of OpportunityProbability in order to test HasOpportunity. So even though both functions are pure, it isn't testable, and I feel like its bad design.
And thus I feel like I'm designing bad Functional Code :)
What I care about with HasOpportunity is mostly the boolean test. Given two doubles and a threshold do the comparison and return the result. In order to get those two doubles it uses a function which requires a list of dates and a date. This leads HasOpportunity to also be responsible for determining the dates (DateTime.Now and 30 days before). Maybe I can split that out:
static bool HasOpportunity(double probability1, double probability2)
{
var threshold = 0.05D;
return probability2 >= threshold && probability1 < threshold;
}
So that is clearly testable. I could even move the threshold up:
static bool HasOpportunity(double threshold, double probability1, double probability2)
{
return probability2 >= threshold && probability1 < threshold;
}
So this is even more generic.
The issue I run into when I do this is that I've just moved things up to Opportunities:
static List<List<DateTime>> Opportunities(List<List<DateTime>> dates)
{
return dates.Where(ds => {
var current = OpportunityProbability(ds, DateTime.Now);
var previous = OpportunityProbability(ds, DateTime.Now.Subtract(TimeSpan.FromDays(30)));
return HasOpportunity(0.05D, current, previous);
}).ToList();
}
This is where I don't know the next step to take.
Any thoughts oh functional overlords? Help me write F# in C#, thanks in advance!
update
So taking it another step, I can get:
static List<List<DateTime>> Opportunities(double threshold, DateTime currentDate, DateTime previousDate, List<List<DateTime>> dates)
{
return dates.Where(ds => {
var current = OpportunityProbability(ds, currentDate);
var previous = OpportunityProbability(ds, previousDate);
return HasOpportunity(threshold, current, previous);
}).ToList();
}
So I still don't know how to test this, but it is nice in that the parameters to this function end up making a definition of what the opportunity is:
- threshold
- first date
- second date
And then given a list of a list of dates it can give you the opportunities.