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.