Reputation: 1906
One major architectural goal when designing large applications is to reduce coupling and dependencies. By dependencies, I mean source-code dependencies, when one function or data type uses another function or another type. A high-level architecture guideline seems to be the Ports & Adapters architecture, with slight variations also referred to as Onion Architecture, Hexagonal Architecture, or Clean Architecture: Types and functions that model the domain of the application are at the center, then come use cases that provide useful services on the basis of the domain, and in the outermost ring are technical aspects like persistence, networking and UI.
The dependency rule says that dependencies must point inwards only. E.g.; persistence may depend on functions and types from use cases, and use cases may depend on functions and types from the domain. But the domain is not allowed to depend on the outer rings. How should I implement this kind of architecture in Haskell? To make it concrete: How can I implement a use case module that does not depend (= import) functions and types from a persistence module, even though it needs to retrieve and store data?
Say I want to implement a use case order placement via a function U.placeOrder :: D.Customer -> [D.LineItem] -> IO U.OrderPlacementResult
, which creates an order from line items and attempts to persist the order. Here, U
indicates the use case module and D
the domain module. The function returns an IO action because it somehow needs to persist the order. However, the persistence itself is in the outermost architectural ring - implemented in some module P
; so, the above function must not depend on anything exported from P
.
I can imagine two generic solutions:
U.placeOrder
takes an additional function argument, say U.OrderDto -> U.PersistenceResult
. This function is implemented in the persistence (P
) module, but it depends on types of the U
module, whereas the U
module does not need to declare a dependency on P
.U
module defines a Persistence
type class that declares the above function. The P
module depends on this type class and provides an instance for it.Variant 1 is quite explicit but not very general. Potentially it results in functions with many arguments. Variant 2 is less verbose (see, for example, here). However, Variant 2 results in many unprincipled type classes, something considered bad practice in most modern Haskell textbooks and tutorials.
So, I am left with two questions:
Upvotes: 6
Views: 617
Reputation: 33978
I'm not strong with Haskell, but I have a pretty clear understanding of Clean Architecture so maybe I can help...
Say I want to implement a use case order placement via a function
U.placeOrder :: D.Customer -> [D.LineItem] -> IO U.OrderPlacementResult
, which creates an order from line items and attempts to persist the order.
I see a red flag in the above. You are assuming that the use case should be attempting to persist the order, but that is decidedly not true. As you said yourself, the placeOrder
function should not depend on persistence, therefore it's not the use case's job to do persistence.
Rather, I would expect a function declaration more like: U.placeOrder :: D.Customer -> [D.LineItem] -> U.PersistenceCommand
. This pure function would be called from the P
module which would pass the result into an IO function which knows how to interpret PersistenceCommand
s and do the actual persistence (P can depend on U, but U can't depend on P).
In essence, the D module contains only data types that the business would understand, the U module contains pure functions and data types describing how those data types in the D module can be manipulated, and the P module takes the descriptions and does the manipulation (persistence, network requests, & ui).
Doing the above is precisely the Functional Core, Imperative Shell, or Impureim Sandwich that Mark Seeman discussed in his answer to this question.
Your variant 1 is a popular solution in imperative/OO languages that don't make the strong distinction between pure and impure functions, but doing so makes the use cases impure and means you will need to use mocks when testing use cases. Using the Impureim Sandwich (FC/IS) approach can also be used in such languages and will result in use cases that don't require mocks to test.
Upvotes: 1
Reputation: 233347
There are, indeed, other alternatives (see below).
While you can use partial application as dependency injection, I don't consider it a proper functional architecture, because it makes everything impure.
With your current example, it doesn't seem to matter too much, because U.placeOrder
is already impure, but in general, you'd want your Haskell code to consist of as much referentially transparent code as possible.
You sometimes see a suggestion involving the Reader
monad, where the 'dependencies' are passed to the function as the reader context instead of as straight function arguments, but as far as I can tell, these are just (isomorphic?) variations of the same idea, with the same problems.
Better alternatives are functional core, imperative shell, and free monads. There may be other alternatives as well, but these are the ones I'm aware of.
You can often factor your code so that your domain model is defined as a set of pure functions. This is often easier to do in languages like Haskell and F# because you can use sum types to communicate decisions. The U.placeOrder
function might, for example, look like this:
U.placeOrder :: D.Customer -> [D.LineItem] -> U.OrderPlacementDecision
Notice that this is a pure function, where U.OrderPlacementDecision
might be a sum type that enumerates all the possible outcomes of the use case.
That's your functional core. You'd then compose your imperative shell (e.g. your main
function) in an impureim sandwich:
main :: IO ()
main = do
stuffFromDb <- -- call the persistence module code here
customer -- initialised from persistence module, or some other place
lineItems -- ditto
let decision = U.placeOrder customer lineItems
_ <- persist decision
return ()
(I've obviously not tried to type-check that code, but I hope it's sufficiently correct to get the point accross.)
The functional core, imperative shell is by far the simplest way to achieve the desired architectural outcome, and it's conspicuously often possible to get away with. Still, there are cases where that's not possible. In those cases, you can instead use free monads.
With free monads, you can define data structures that are roughly equivalent to object-oriented interfaces. Like in the functional core, imperative shell case, these data structures are sum types, which means that you can keep your functions pure. You can then run an impure interpreter over the generated expression tree.
I've written an article series about how to think about dependency injection in F# and Haskell. I've also recently published an article that (among other things) showcases this technique. Most of my articles are accompanied by GitHub repositories.
Upvotes: 3