4

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:

  1. Higher order functions: The function 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.
  2. Type classes: The 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:

  • Am I missing other alternatives?
  • Which approach is the generally recommended one, if any?
Ulrich Schuster
  • 1,670
  • 15
  • 24
  • Please clarify using your favourite OO language how you'd achieve what you're trying to do. – is7s Aug 14 '21 at 17:00

1 Answers1

3

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.

Functional core, imperative shell

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.)

Free monads

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.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • Mark, thanks for the great answer and the very helpful blog posts. I'd like to dig a little deeper, to arrive at where I am stuck. In my simplified example, I am talking about the middle shell of the clean architecture, not the domain but the use cases. In a large application, there will be many of them. Simply pushing the use case logic to the `main` function won't do. So, use cases probably need to be impure (return IO actions). Yet, I want to avoid that use case modules depend on details of persistence or UI. The sandwich does not seem to answer this question. – Ulrich Schuster May 14 '20 at 14:40
  • I've read about free monads, but for a Haskell beginner they are hard to grok, so I decided to get some substantial programs up and running before I take a look at the. But from what I get so far, it seems strange to write my own interpreter for basic tasks. This seems to imply to invent my own DSL for each problem - which for most enterprise tasks will mean lots of duplication and finally someone writing books about "functional enterprise patterns". But requiring patterns seems to imply some deficiency in the language - which is why I want to move from OOP to FP in the first place. – Ulrich Schuster May 14 '20 at 14:45
  • @UlrichSchuster Whether or not you can use the impureim sandwhich architecture has little to do with the distinction between domain model and use cases. It mostly [correlates with the type of application](https://blog.ploeh.dk/2017/07/10/pure-interactions). For example, if you're implementing a REST API, I see no reason for use cases to be impure. Each HTTP request is already impure (like `main`) so that's where you can please each impureim sandwich. – Mark Seemann May 14 '20 at 14:56
  • @UlrichSchuster Writing an interpreter for a free monad is like implementing an interface in OOP. Why would it lead to duplication? – Mark Seemann May 14 '20 at 14:57
  • In you example of a ReSTful server, wouldn't `main` be a rather small function that contains the (impure) main event loop, probably provided by some library like Scotty or Servant? Depending on the route, this `main` function invokes different function for each use case, implemented in one or more use case modules. According to clean architecture, the goal is that these modules do neither depend on the HTTP library nor on any persistence library. But the persistence or HTTP functionality must be implemented somewhere - I don't imagine all SQL queries in `Main.hs`. – Ulrich Schuster May 14 '20 at 15:24
  • @UlrichSchuster Indeed. Consider [this Servant API implementation](https://github.com/ploeh/reservation-api-slice-haskell/blob/master/src/ReservationAPI.hs), which doesn't depend on the persistence module. It does mix together HTTP models and domain models, because my motivation for writing that sample code base wasn't to demonstrate how to implement that separation, but you can. Notice that everything in that module is pure. – Mark Seemann May 14 '20 at 15:39
  • @UlrichSchuster hello - this may be of interest: Functional Core and Imperative Shell - Game of Life Example - Haskell and Scala https://www.slideshare.net/pjschwarz/functional-core-and-imperative-shell-game-of-life-example-haskell-and-scala – Philip Schwarz Oct 10 '21 at 21:50