Home > OS >  F# - Dependencies management
F# - Dependencies management

Time:09-26

I'd like to understand concept of dependencies management from functional paradigm perspective. I attempted to apply concept of dependency rejection and from what I understood it all boils down to creating a "sandwich" from impure [I/O] and pure operations and passing only values to pure functions while performing any I/O operations at the edge of system. The thing is, that I still have to somehow obtain results from external sources and this is where I'm stuck.

Consider below code:

[<ApiController>]
[<Route("[controller]")>]
type UserController(logger: ILogger<UserController>, compositionRoot: CompositionRoot) =
    inherit BaseController()
    
    [<HttpPost("RegisterNewUser")>]
    member this.RegisterNewUser([<FromBody>] unvalidatedUser: UnvalidatedUser) = // Receive input from external source: Impure layer
        User.from unvalidatedUser            // Vdalidate incoming user data from domain perspective: Pure layer
        >>= compositionRoot.persistUser      // Persist user [in this case in database]: Impure layer
        |> this.handleWorkflowResult logger  // Translate results to response: Impure layer

CompositionRoot along with logger are injected via dependency injection. This is done in such a way for two reasons:

  • I don't really know how to obtain those dependencies in other, functional way than DI.
  • In this particular case CompositionRoot requires database repositories which are based on EntityFramework which are also obtained through DI.

Here is the composition root itself:

type CompositionRoot(userRepository: IUserRepository) = // C# implementation of repository based on EntityFramework
    member _.persistUser = UserGateway.composablePersist userRepository.Save
    member _.fetchUserByKey = UserGateway.composableFetchByKey userRepository.FetchBy

The above does not look any different to me than "standard" dependency injection done in C#. The only difference I can see is that this one is operating on functions instead of abstraction-implementation pairs and that it is done "by hand".

I searched over the internet for some examples of dependencies management in larger project, but what I found were simple examples where one or two functions were passed at most. While those are good examples for learning purposes I can't really see it being utilised in real-world project where such "manual" dependencies management can quickly spiral out of control. Other examples regarding external data sources such as databases presented methods which expected to receive connection string, but this input has to be obtained from somewhere [usually through IConfiguration in C#] and hardcoding it somewhere in composition root to pass it to composed function is obviously far from ideal.

The other approach I found was combination of multiple dependencies into single structure. This approach is even more similar to standard DI with "interfaces" that are, again composed by hand.

There is also a last concern that I have: What about functions that call other functions that require some dependencies? Should I pass those dependencies to all the functions down to the bottom?

let function2 dependency2 function2Input =
    // Some work here...
    
let function1 dependency1 dependency2 function1Input =
    let function2Input = ...
        
    function2 dependency2 function2Input
    
// Top-level function which receives all dependencies required by called functions
let function0 dependency0 dependency1 dependency2 function0Input =
    let function1Input = ...
        
    function1 dependency1 dependency2 function1Input

The last question is about composition root itself: Where should it be located? Should I build it in similar fashion like in C# Startup where I register all services or should I create separate composition roots specific to given workflow / case? Either of those approaches would require me to obtain necessary dependencies [like repositories] from somewhere in order to create composition root.

CodePudding user response:

There's more than one question here, but I'll try my best to answer them in order.

First, you'll need to weigh advantages and disadvantages of different architectural decisions. Why inject dependencies into Controllers in the first case?

This can be a good idea if you want to subject Controllers to certain kinds of automated testing. I usually do this with state-based integration testing. There is, however, another school of thought that insist that you shouldn't unit test Controllers. The idea is that Controllers should be so drained of logic that unit testing isn't worth the trouble. In such cases, you don't need Dependency Injection (DI) at that level - at least, not for testing purposes. Instead, you can leave the actual database code in the Controller without having to resort to any DI. Any testing, then, will have to involve a real database (although that can also be automated).

This would be a valid architectural choice to make, but for the sake of argument, let's assume that you'd at least want to inject depedencies into the Controllers, so that you can do some automated testing.

In C#, I'd use interfaces for that, and I'd also use interfaces in F#. There's no reason to confuse co-workers with free monads. Passing functions around might also be viable, though.

I searched over the internet for some examples of dependencies management in larger project

Yes, this is a known problem (also in OOD, for that matter). There's a dearth of complex examples, for fairly obvious reasons: Real-world examples are proprietary and typically not open source, and few people will spend a couple of months of their free time to develop a sufficiently complex example code base.

To address that lack, I've developed such a code base to accompany my book Code That Fits in Your Head. That code base is in C# rather than F#, but it does follow the Impureim Sandwich architecture (AKA functional core, imperative shell). I hope you'll be able to learn from that example code base. It juggles a small handful of impure dependencies, but keep them all constrained to Controllers.

What about functions that call other functions that require some dependencies?

In FP, you should endeavour to write pure functions. While you can compose functions from other functions (higher order functions), they should all still be pure. Thus, it's not really idiomatic to compose functions of other functions that have impure dependencies, because that makes the entire composition impure.

Instead, keep all impure dependencies at the boundary of the system (e.g. Controllers) and compose everything else from pure functions.

composition root itself: Where should it be located?

If you need one at all (re: the first point about testability), it'll be equivalent to C#. Put it close the application's entry point.

Regardless of language, I prefer pure DI for that.

CodePudding user response:

If you're trying to write pure functional code, then dependency injection isn't helping much here, since the injected functions (persistUser, fetchUserByKey, and handleWorkflowResult) are themselves impure. Thus, anything that calls these functions (e.g. RegisterNewUser) is also impure.

So how do we separate (i.e. "reject") the impure dependencies from the pure functional business logic? One way to do this in F# is by defining computation expressions that you can then use to build pure functional computations, like this:

// Pure functional code with no dependencies.
let registerNewUser unvalidatedUser =
    stateful {
        let user = User.from unvalidatedUser
        do! persistUser user            // NOTE: doesn't actually persist anything yet
        do! handleWorkflowResult user   // NOTE: doesn't actually log anything yet
    }

You can then run the stateful computation from the edge of your system:

// Impure method with external dependencies.
member this.RegisterNewUser(unvalidatedUser: UnvalidatedUser) =
    registerNewUser unvalidatedUser
        |> Stateful.run compositionRoot   // NOTE: this is where actual persistence/logging occurs

Scott Wlaschin call this approach "dependency interpretation".

Important caveat: Many F# developers would consider this overkill for a simple system. I'm only suggesting it here to show how impure dependencies can be handled in a (mostly) pure functional way, which I think is what you're asking for.

  • Related