My project architecture has been setting up to allow dependency injection. For instance, the commands take repository instances as arguments. But the approach I’ve been preparing is very object-oriented, and in my notes I had mused on thoughts about how partial application would be a more functional way of doing this. However exactly how to structure this has eluded me. Thankfully the ever understandable F# for Fun and Profit just created a post Functional approaches to dependency injection that bridges this gap, so now I’m going to walk through my conversion from interfaces to function types.
Interfaces to Function Types
The repositories the simplest place to start. At present the interface for the template repositories is:
type ITemplateReadRepository = abstract member FindById: System.Guid -> Template option type ITemplateWriteRepository = abstract member FindById: System.Guid -> Template option abstract member Save: Template -> unit
Changing these to function types means replacing each member with a function type.
I firmly believe in read-write separation so it’s important that there is a distinction made between finds made by the write system for the purpose of identity and validation, and finds made by a read system for querying. So despite having identical signatures, I like the concept of different types for
type FindTemplateById = System.Guid -> Template option type FindTemplateByIdForValidation = System.Guid -> Template option type SaveTemplate = Template -> unit
In a large project these would likely be separated into different modules purely for the purpose of code organization.
The current implementations of these methods directly instantiate a data context, which means they create a dependency, which is what we’re trying to avoid.
member this.FindById (id:Guid) = let dc = Sql.GetDataContext() // use dc to find template
In object-oriented dependency injection the domain class would have a dependency on some IRepository and the IoC container would create a repository instance passing in the connection information. In functional programming this option is not available, so the dependencies have to be passed as function arguments meaning we need a method with this signature:
DbContext -> Guid -> Template option // for example, the persistence method: let findTemplateById (dc:DbContext) (id:Guid) = ...
However this means the caller has to know how to create the
DbContext dependency. That is likely not the responsibility of the caller, so we need another abstraction that manages the dependencies and only requires caller to provide variables that it is responsible for. We can do this by providing a function which can convert between the signature understood by the caller and the signature of the target method.
// Persistence method: DbContext -> Guid -> Template option let FindTemplateById dc id = ... // Domain method: (Guid -> Template option) -> Guid -> Template option let GetTemplate findById id = findById id // Caller let dc = Sql.GetDataContext() let findTemplateByIdWithDc = FindTemplateById dc // Signature Converter let res = GetTemplate findTemplateByIdWithDc id
The converting function,
findTemplateByIdWithDc, is a partially applied function of
FindTemplateById because we have not specified all of the arguments, leaving the second (
id) to be set when
findById is called.
In my project the
DbContext instance is created in the program.fs which is a layer higher than the caller function (my Api level) above. This same pattern can be applied so that the
DbContext is passed transparently through the Api level as well as the Domain. For the sake of organization, all of these ‘signature converters’ are placed into a file given the name
CompositionRoot. That file is defined immediately before the first file that uses it, in this case before program.fs. The end result looks something like the following, which is a snapshot of the full stack used for the GET template request.
type FindTemplateById = System.Guid -> Template option // domain/persistence type IGetTemplate = System.Guid -> Template option // api/domain // Persistence.fs module Persistence.TemplateReadRepo = let findById dc id = // use dc ... // (Domain)Operations/Template.fs module Operations.Template = let GetTemplate (findById:FindTemplateById) id = findById id // Api/TemplateController.fs module Api.Template = let getTemplate (getTemplateById:IGetTemplate) (id:Guid) : WebPart = match (getTemplateById id) with ... // CompositionRoot.fs module Operations = let getTemplate dc = AmApi.Operations.Template.GetTemplate (Persistence.TemplateReadRepo.findById dc) module ApiMethods = let getTemplate dc = Api.Template.getTemplate (Operations.getTemplate dc) // Program.fs let route dc = choose [ pathScan ... (ApiMethods.getTemplate dc) ...
The composition root creates partial functions like
Operations.getTemplate dc which mean that the argument given to Api.Template.getTemplate still conforms to the signature it requires while the information about the context travels to the domain, and in a similar fashion to the persistence call where it is finally used.