I'm your classic OOP developer. However since I discovered purely functional programming languages I've been ever intrigued to the why since OOP seemed to solve most business cases in a reasonable manner.
I've now come to the point in my software development experience where I'm seeking more concise and expressive languages. I usually write my software in C# but for my latest project I decided to take the leap and build a business service using F#. In doing so I'm finding it very hard to understand how decoupling is done with a purely functional approach.
The case is this. I have a data-source, which is WooCommerce, but I don't want to tie my function definitions to that specific data source.
In C# it is apparent to me that I want a service that looks something like this
public record Category(string Name);
public interface ICategoryService
{
Task<IEnumerable<Category>> GetAllAsync();
}
// With a definition for the service that specifies WooCommerce
public class WcCategoryService : ICategoryService
{
private readonly WCRestEndpoint wcRest;
// WooCommerce specific dependencies
public WcCategoryService(WCRestEndpoint wcRest)
{
this.wcRest = wcRest;
}
public Task<IEnumerable<Category>> GetAllAsync()
{
// Call woocommerce REST and map the category to our domain category
}
}
Now in the future, if and when I decide we need a new store for providing categories I can define a new implementation for that specific service, replace the injected type and not mess up the dependents because of this change.
Trying to understand how the functional dependency approach is solved I was met with this case (reading "Domain Modeling made functional") where the type signature directly define the dependencies, so the above C# equivalent would turn into a highly coupled definition
type Category = { Name: string }
type GetCategories =
WCRestEndpoint
-> Category list
Suddenly if I am to change the source of categories I would have to either change the functional signature or provide a new definition to be used which would ripple through the application and thereby not be very robust.
What I'm curious about is whether I'm misunderstanding something fundamental.
With my OOP brain all I can think of doing is something like this
type Category = { Name: string }
// No longer directly dependent on WCRestEndpoint
type GetCategories = unit -> Category list
// But the definition would require scoped inclusion of the dependency
// Also how does the configuration get passed in without having the core library be dependent on the Environment or a config in the assembly?
let rest = WCRestEndpoint(/* Config... */)
type getCategories: GetCategories =
fun () ->
let wcCategories = rest.GetCategories()
// Convert the result into a Category type
I've looked around and I haven't found any explanation as to how change is handled with a purely functional approach, which is what led me to believe there is something fundamental I've misunderstood.
How do you expose a functional API without tying the function type signatures up in implementation specific types? Am I thinking about this wrong?