Functional Architecture - Ports and Adapters
- Transfer
What is interesting about functional architecture? It tends to fall into the so-called “Pit of Success”, in which developers find themselves in a situation that forces them to write good code.

When discussing object-oriented architecture, we often come across the idea of port and adapter architecture, although we often call it otherwise: multilevel, onion, or hexagonal architecture. The point is to separate the business logic from the details of the technical implementation so that we can vary them independently. This allows us to maneuver in response to changes in business or technology.
Ports and Adapters
The idea behind port and adapter architecture is that ports represent application boundaries. A port is what interacts with the outside world: user interfaces, a message queue, a database, files, command line prompts, and so on. While ports are the application interface for the rest of the world, adapters provide translation between ports and the application model.

The term “adapter” was chosen successfully, since the role of the adapter ( as a design pattern ) is to provide communication between two different interfaces.
As I explained earlier, you should resort to some kind of ports and adapters if you are using Injection Dependency.
However, the problem with this architecture is that it seems that a lot of explanation is required to implement it:
- my book on Dependency Injection is 500 pages long;
- Robert Martin's book on SOLID principles, package design, components, etc. also takes 700 pages;
- Problem-oriented programming - 500 pages;
- etc…
In my experience, the implementation of the architecture of ports and adapters is Sisyphus labor. It requires a lot of diligence, but if you distract for a moment, the boulder will again roll down.

Implementing the ports and adapters architecture in object-oriented programming is possible, but it takes a lot of effort. Should it be so hard?
Haskell as a tutorial
With a genuine interest in functional programming, I decided to learn Haskell. Not that Haskell was the only functional language, but it provides cleanliness at a level not reachable by either F #, Clojure, or Scala. In Haskell, a function is pure unless its type indicates otherwise. This forces you to be careful in design and to separate pure functions from functions with side effects.
If you don’t know Haskell, code with side effects can only appear inside a specific “context” called IO (input / output). This is a monadic type, but this is not the main thing. The main thing is that by the type of function you can say whether it is pure or not. Function with type
ReservationRendition -> Either Error Reservation
is clean because it is not
IOin the type. On the other hand, a function with type:ConnectionString -> ZonedTime -> IO Int
not clean, because the type she returns is -
IO Int. This means that the return value is an integer, but this integer comes from the context in which it can change between function calls. There is a fundamental difference between functions that return
Intand IO Int. In Haskell, any function that returns Intis link-transparent en.wikipedia.org/wiki/Referential_transparency . This means that the function is guaranteed to return the same value on the same input. On the other hand, the function returning IO Intdoes not give such a guarantee.When writing programs in Haskell, you should strive to maximize the number of pure functions by shifting unclean code to the boundaries of the system. A good Haskell program has a large core of pure functions and an I / O shell. Looks familiar, doesn't it?
In general, this means that the Haskell type system provides port and adapter architecture. Ports are your input / output code. The core of the application is all your pure functions. The type system automatically pushes you into the "pit of success."

Haskell is a great learning aid because it makes you clearly distinguish between pure and impure functions. You can even use it as a tool to check if your F # code is “sufficiently functional”.
F # is primarily a functional language, but it also allows you to write object-oriented or imperative code. If you write your code in F # in a “functional” way, it is easy to translate it to Haskell. If your F # code is hard to translate to Haskell, it's probably not functional.
Below is a live example for you.
Accepting armor on F #, first attempt
In my Pluralsight course Test-Driven Development with F # (an abbreviated free version is available: http://www.infoq.com/presentations/mock-fsharp-tdd ) I demonstrate how to implement the HTTP API for an online restaurant reservation system, which accepts reservation requests. One of the steps when processing a reservation request is to check if there are enough free seats in the restaurant to accept reservations. The function looks like this:
// int
// -> (DateTimeOffset -> int)
// -> Reservation
// -> Result
let check capacity getReservedSeats reservation =
let reservedSeats = getReservedSeats reservation.Date
if capacity < reservation.Quantity + reservedSeats
then Failure CapacityExceeded
else Success reservation
As the commentary suggests, the second argument
getReservedSeatsis a type function DateTimeOffset -> int. The function checkcalls it to get the number of already reserved seats at the requested date. During unit testing, you can replace a pure function with a stub, for example:
let getReservedSeats _ = 0
let actual = Capacity.check capacity getReservedSeats reservation
And during the final assembly of the application, instead of using a clean function with a fixed fixed return value, you can make an unclean one that queries the database to obtain the required information:
let imp =
Validate.reservation
>> bind (Capacity.check 10 (SqlGateway.getReservedSeats connectionString))
>> map (SqlGateway.saveReservation connectionString)
Here
SqlGateway.getReservedSeats connectionStringis a partially applied function of type - DateTimeOffset -> int. In F # you cannot say by type that it is unclean, but I know that it is because I wrote this function. The function queries the database, therefore it is not reference clean. All this works well in F #, where it depends on you whether a particular function will be clean or unclean. As
impa member of the Composition root of this application, the impure functions SqlGateway.getReservedSeatsand SqlGateway.saveReservationonly appear on the boundary of the system. The rest of the system is well protected from side effects. It looks functional, but is it really so?
Haskell Feedback
To answer this question, I decided to remake the main part of the application on Haskell. My first attempt to check available seats was directly translated as follows:
checkCapacity :: Int
-> (ZonedTime -> Int)
-> Reservation
-> Either Error Reservation
checkCapacity capacity getReservedSeats reservation =
let reservedSeats = getReservedSeats $ date reservation
in if capacity < quantity reservation + reservedSeats
then Left CapacityExceeded
else Right reservation
This compiles and at first glance seems promising. Function Type
getReservedSeats- ZonedTime -> Int. Since it IOdoes not appear anywhere in this type, Haskell ensures that it is clean. On the other hand, when you need to implement a function to retrieve the number of reserved places from the database, it will by its nature have to become unclean, since the return value can change. To include this in Haskell, a function must be of the type:
getReservedSeatsFromDB :: ConnectionString -> ZonedTime -> IO Int
Although you can partially apply the first argument
ConnectionString, the return value will be IO Int, not Int. A type function
ZonedTime -> IO Intis not the same as ZonedTime -> Int. Even when executed inside an IO context, you cannot convert ZonedTime -> IO Intto ZonedTime -> Int. On the other hand, you can call an impure function inside the IO context and extract
Intfrom IO Int. This does not quite match the above function checkCapacity, so you will need to review its design. Although the code on F # looked “quite functional”, it turns out that this design is not really functional. If you look closely at the function above
checkCapacity, you may wonder why it is necessary to transfer a function to determine the number of reserved seats. Why not just pass that number?checkCapacity :: Int -> Int -> Reservation -> Either Error Reservation
checkCapacity capacity reservedSeats reservation =
if capacity < quantity reservation + reservedSeats
then Left CapacityExceeded
else Right reservation
So much easier. At the system boundary, the application runs in an IO context, allowing you to create clean and unclean functions:
import Control.Monad.Trans (liftIO)
import Control.Monad.Trans.Either (EitherT(..), hoistEither)
postReservation :: ReservationRendition -> IO (HttpResult ())
postReservation candidate = fmap toHttpResult $ runEitherT $ do
r <- hoistEither $ validateReservation candidate
i <- liftIO $ getReservedSeatsFromDB connStr $ date r
hoistEither $ checkCapacity 10 i r
>>= liftIO . saveReservation connStr
(full source code is available here: https://gist.github.com/ploeh/c999e2ae2248bd44d775 )
Do not worry if you do not understand all the details of this composition. I described the main points below:
The function
postReservationreceives an input ReservationRendition(consider this a JSON document) and returns IO (HttpResult ()). IOinforms you that all this function is performed in the IO monad. In other words, the function is unclean. This is not surprising since it is a system boundary. Also, note that the function
liftIOis called twice. You do not need to understand in detail what it does, but it is necessary to “pull out” the value from the IO type; i.e., for example, pull IntoutIO Int. Thus, it becomes clear where the code is clean and where it is not: the function liftIOis applied to getReservedSeatsFromDBand saveReservation. This suggests that these two functions are unclean. By the method of exclusion, the remaining functions ( validateReservation, checkCapacityand toHttpResult) are pure. The question also arises of how one can alternate pure and impure functions. If you look closely, you will see how data is transferred from a pure function
validateReservationto an impure function getReservedSeatsFromDB, and then both return values ( rand i) are transferred to a pure function checkCapacityand, finally, to the unclean save function saveReservation. All this happens in a block (EitherT Error IO) () do, so if any of these functions returnsLeft, the function closes and produces a final error. For a clear and visual introduction to Either type monads, see Scott Wlaschin's excellent article, Railway Oriented Programming (EN). The value from this expression is obtained using the built-in function
runEitherT; and again with this clean function:toHttpResult :: Either Error () -> HttpResult ()
toHttpResult (Left (ValidationError msg)) = BadRequest msg
toHttpResult (Left CapacityExceeded) = StatusCode Forbidden
toHttpResult (Right ()) = OK ()
The entire function is
postReservationunclean and is located at the boundary of the system, since it processes IO. The same applies to the functions getReservedSeatsFromDBand saveReservation. I intentionally put two functions for working with the database at the bottom of the diagram below, so that it seems more familiar to readers who are accustomed to multi-level architectural diagrams. You can imagine that under the circles there are cylindrical objects representing databases.
You can consider the functions
validateReservationand toHttpResultas belonging to the application model. They are clean and translate between external and internal data representation. Finally, if you want, the function checkCapacityis part of the application domain model. Most of the design of my first attempt at F # was preserved, except for the function
Capacity.check. Re-implementing design in Haskell taught me an important lesson that I can now apply to my code in F #.Reception of armor on F #, even more functional
The changes required are small, so the lesson from Haskell is easy to apply to F # -based code. The main culprit was the function
Capacity.check, which should be implemented as follows:let check capacity reservedSeats reservation =
if capacity < reservation.Quantity + reservedSeats
then Failure CapacityExceeded
else Success reservation
This not only simplifies the implementation, but also makes the composition a little more attractive:
let imp =
Validate.reservation
>> map (fun r ->
SqlGateway.getReservedSeats connectionString r.Date, r)
>> bind (fun (i, r) -> Capacity.check 10 i r)
>> map (SqlGateway.saveReservation connectionString)
This looks a little more complicated than the Haskell function. The advantage of Haskell is that you can automatically use any type that implements the class
Monadinside the block do, and since it (EitherT Error IO) ()is an instance Monad, the syntax is dofree. You can do something similar in F #, but then you have to implement your own constructor of computational expressions for the Result type. I described it on my blog .
Summary
Good functional design is equivalent to the architecture of ports and adapters. If you use Haskell as a criterion for an “ideal” functional architecture, you will see how its explicit distinction between pure and impure functions creates the so-called “pit of success”. If you don’t write your entire application inside the IO monad, Haskell will automatically reflect the difference and push all communication with the outside world to the borders of the system.
Some functional languages, such as F #, do not use this distinction explicitly. However, in F # it is easy to unofficially implement it and build applications with unclean functions located at the borders of the system. Although this distinction is not imposed by the type system, it still seems natural.
If the topic of functional programming is more relevant for you than ever, you will probably be interested in these reports from our two-day November conference DotNext 2017 Moscow :
- light but addictive story by Nikolai Gusev about functional programming for C #
- interesting report for practitioners from pattern expert Mark Seeman " From dependency injection to dependency rejection "
- a practical and useful story by Roman Nevolin about type providers: how to use them, what problems they solve and how to write them
- and Andrey Akinshinin’s keynote on “ performance testing ”.