Functional programming and decoupling
Asked Answered
V

6

38

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?

Varick answered 28/6, 2021 at 9:42 Comment(2)
Not an answer, but there's a lot written on blog.ploeh.dk about functional architectures.Sidonius
BTW, be careful with the term category when talking to functional programmers.Centroclinal
S
58

I struggled with this question for years before I realised that I was looking at it the wrong way. Coming from object-oriented development and Dependency Injection, I kept looking for a functional alternative to Dependency Injection. I finally realised that Dependency Injection makes everything impure, which means that you can't use that approach (not even partial application) if you want to apply a functional architecture.

The red herring is to focus on the dependencies. Instead, focus on writing pure function. You can still use the Dependency Inversion Principle, but instead of focusing on actions and interactions, focus on data. If a function requires some data, pass it as an argument. If a function has to make a decision, return it as a data structure.

You don't provide any examples of where you'd want to use a list of Category values, but a function that depends on such data would have a type like this:

Category list -> 'a

Such a function is completely decoupled from the source of the categories. It only depends on the Category type itself, which is part of the Domain Model.

Ultimately, you'll need to get the categories from somewhere, but this work you push to the boundary of the system, e.g. Main:

let Main () =
    let categories = getCategories ()
    let result = myFunction categories
    result

Thus, if you change your mind about how to get the categories, you only have to change one line of code. This kind of architecture is akin to a sandwich, with impure actions surrounding the pure heart of the application. It's also known as functional core, imperative shell.

Sizing answered 28/6, 2021 at 10:34 Comment(3)
I think it’s very misleading to say that dependency injection is impure (at least in general). Algebraic effects, for example, are a robust way to inject different dependencies while leaving all of the code except the dependency pure. (Monads of course model this too, but dependency injection takes more elbow grease with just monads, such as transformer stacks.) What do you mean when say that dependency injection makes everything “impure?”Evanne
@Evanne It's a catchphrase that I believe is true for most non-degenerate Dependency Injection (DI). There are corner cases where it doesn't hold, but apart from the occasional injected Strategy most counter-examples would be degenerate. Most real uses of DI involves impure actions, and once you have an impure action, all callers are also transitively impure. I hope that the resources I linked to sufficiently elaborate on these points, including awareness of alternatives. I don't consider the phrase misleading.Sizing
I probably should have said that it is ambiguous more than misleading. I think my disagreement hinges on what we’re calling “impure.” DI probably does mean that the running program is impure, but the running program pretty much always has to be impure. However, it doesn’t mean that the program itself is written impurely: you can write a program in such a way that it’s 100% pure until someone supplies an impure effect evaluator. I do think there is a “contagious” aspect to DI (similar to “function coloring” with async/await) that can cause sprawl. That may be close to what you’re getting at.Evanne
M
22

I don't think there is a single right answer to this, but here is a couple of points to consider.

  • First, I think real-world functional code often has a "sandwich structure" with some input handling, followed by purely functional transformation and some output handling. The I/O parts in F# often involve interfacing with imperative and OO .NET libraries. So, the key lesson is to keep the I/O to the outside and keep the core functional handling separate from that. In other words, it makes perfect sense to use some imperative OO code on the outside for input handling.

  • Second, I think the idea of decoupling is something that is more valuable in OO code where you expect to have complex interfaces with intertwined logic. In functional code, this is (I think) much less of a concern. In other words, I think it is perfectly reasonable not to worry about this for I/O, because it is only the outer side of the "sandwich structure". If you need to change it, you can just change it without touching the core functional transformation logic (which you can test independently of I/O).

  • Third, on the practical side, it is perfectly reasonable to use interfaces in F#. If you really wanted to do the decoupling, you could just define an interface:

    type Category { Name: string }
    
    type CategoryService = 
       abstract GetAllAsync : unit -> Async<seq<Category>>
    

    And then you can implement the interface using object expressions:

    let myCategoryService = 
      { new CategoryService with 
        member x.GetAllAsync() = async { ... } }
    

    Then, I would have a main function that transforms the seq<Category> into whatever result you want and this would not need to take CategoryService as argument. However, in your main bit of code, you would take this as argument (or initialize it somewhere when the program starts), use the service to fetch the data and call your main transformation logic.

Mallemuck answered 28/6, 2021 at 10:20 Comment(1)
I wish I could accept this answer as well. This really helped me understand where my thinking was lacking regarding the functional paradigm.Varick
D
5

If all you want is to not use objects, it's a fairly mechanical rewrite.

A single-method interface is just a named function signature, so this:

public interface ICategoryService
{
    Task<IEnumerable<Category>> GetAllAsync();
}

async Task UseCategoriesToDoSomething(ICategoryService service) {
    var categories = await service.GetAllAsync();
    ...
}

becomes:

let useCategoriesToDoSomething(getAllAsync: unit -> Async<seq<Category>>) = async {
    let! categories = getAllAsync()
    ...
}

Your composition root becomes a matter of partially applying functions with the concrete implementations of these function arguments.

That said, there is nothing wrong with using objects as such; F# mostly rejects mutability and inheritance, but embraces interfaces, dot notation, etc.

There's a great slide on OO in F# from a talk Don Syme gave: enter image description here

Domingodominguez answered 28/6, 2021 at 13:44 Comment(0)
R
3

You need an asynchronously computed list of categories. There's already a type for that: Async<seq<Category>>. Maybe this was created by a function using a WCRestEndpoint. Maybe this was created with some constant dummy values in a unit test. Maybe this was created for a unit test and always raises an error. The consuming code doesn't care. It just cares that there is a way to get the categories.

This type is much more reusable than an application-specific ICategoryService type. For example, maybe you have a function that takes Async<'a> and handles errors in a standard way. Maybe you have a function that takes an Async<seq<'a>> and validates that the list isn't empty.

You honestly don't need a special name just for the thing that fetches one kind of thing.

Reflexion answered 29/6, 2021 at 5:15 Comment(3)
Quite right to distinguish what this function needs - a list-of-categories-available-when-they're-ready from some "interface" that ties it to a "category service" - whatever that is. (Especially since whatever that is, a "category service", the interface ICategoryService provides (directly or indirectly through inheritance) cruft this function doesn't need, e.g., how to start and stop the "service" - and having that tied to this use is a totally unnecessary dependency.) (But this applies to "normal" O-O programming as well ... though you frequently don't see it used right ...)Fordone
By the way, this is an example of something that plagues programming in general, and O-O programming in particular. A particular paradigm - especially a popular one like O-O - has problems in applying it to real use cases. So tools get developed to alleviate those problems. Simple at first, they're so effective people build more of them and build on them and they get more complex and more flexible .. and then ... the tool takes over the paradigm! The OP and others may think of "dependency injection" as being part of O-O and because the tool is capable of injecting services ...Fordone
... it becomes easy to think of injection of services as being part of O-O design. But it isn't: a principle of O-O design in minimizing dependencies by isolating design decisions ... but you wouldn't necessarily know that if you had bought a bunch of books on how to use Spring or Hibernate or something and were working with a bunch of people who were steeped in that culture too ...Fordone
P
3

Congratulations on making the very good choice to try F#!

To answer your question in yet another way:

@Asik already mentioned using a function instead of a single-method interface. This idea may very well be extended to having records which group a bunch of related functions together; for example:

type MyEntityRepository =
  { Fetch: Guid -> Async<MyEntity>
    Add: MyEntity -> Async<unit>
    Delete: Guid -> Async<unit> }

It's always possible to use interfaces as well, but I like this method better because it's easier to mock (just assign Unchecked.defaultof<_> to any fields the test code won't use) and because the syntax looks much nicer, among other things.

If you need nested dependencies (which you surely will), you can simply use closures:

let createRepository (connection: IDbConnection) =
  { Add = fun entity -> connection.Execute(...)
    Fetch = fun id -> connection.Query(...) }

You essentially provide the dependencies to a factory function and then the dependencies will be captured in the lambdas' closures, allowing you to nest dependencies as deeply as need be. This pattern of using factory functions works quite well with ASP.Net's built-in DI container too.

Prolepsis answered 29/6, 2021 at 20:47 Comment(0)
D
1

First, as @Karl Bielefeldt has been pointed out, the correct type to return here is Async<seq<Category>>. So your function originally should be of type WCRestEndpoint -> Async<seq<Category>>.

But that's not the real issue here. The real issue is this claim:

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.

This claim does not make any sense at all to me because refactoring is actually simpler in the F# case.

No matter how you code this, you're always going to need to write code that takes a WCRestEndpoint and outputs a sequence of Categorys. If you decide that you're actually going to get the sequence of Categorys in some other way, then you're going to need to write new code to do this no matter what.

For example, let's suppose I decide I need to modify my code to get categories from a OtherCatGetter instead of a WCRestEndpoint. In your C# code, I would need to replace

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
    }
}

with

public class OtherCategoryService : ICategoryService
{
    private readonly OtherCatGetter getter;

    // WooCommerce specific dependencies
    public WcCategoryService(OtherCatGetter getter)
    {
        this.getter = getter;
    }

    public Task<IEnumerable<Category>> GetAllAsync()
    {
        // Do something with getter to get the categories
    }
}

We would also have to replace every single call to new WcCategoryService(wcRest) with a call to new OtherCategoryService(getter).

On the F# side, we would have to replace

let getCategoriesFromWC (wcRest: WCRestEndpoint) = ... // get categories from wcRest

with

let getCategoriesFromOther (getter: OtherCatGetter) = ... // get categories from getter

and replace every occurence of GetCategoriesFromWC wcRest with an occurence of getCategoriesFromOther getter.

Clearly, it's the F# version that requires less refactoring when you need to change how you're getting your Categorys, since F# doesn't have to deal with the boilerplate of defining a new public class with a single read-only field and a one-argument constructor. If you need to define a different way of getting a sequence of Categorys, you just do it rather than jumping through unnecessary hoops.

Daph answered 1/7, 2021 at 0:48 Comment(3)
I see your point and it's definitely not obvious in what context the ICategoryService was being used. In my case I'm wiring the service into Microsofts dependency injection library and so every ASP.NET controller would only have to reference the interface rather than the actual implementation and all I'd have to do to wire up a new service would be to refactor the wiring of IServiceCollection. This would centralize the affected area to a single place (in my Startup.fs or ServiceCollectionExtension.fs) instead of each occurrence as in your example.Varick
@BjarkeSporring I'm not an expert on C#, but it sounds like you're saying that you would only need to call new WcCategoryService(wcRest) once in your C# code, which makes refactoring simple. But if that's the case, then you only need to call getCategoriesFromWC wcRest once in your F# code, so you can centralize the affected area to only a single place in your F# code in exactly the same way.Daph
Yes that is exactly what I'm trying to do and you might be right which is what I'm investigating right now. I'll update the post with my once I have a firm grasp on exactly how. Thank you for the perspective btw.Varick

© 2022 - 2024 — McMap. All rights reserved.