My take on functional programming

Coming from a very object-oriented background, I have been quite unaware of the trend of functional programming, and as it happens when you face something unknown, it took me some courage to finally do some research about it. How is it possible that objects are not needed? What if my foundations about what I know start shaking and crumbling, and I have to re-learn everything? (Well that fear is happening all the time, without the threat of new concepts, that's why I keep back checking the books again and again).

The thing is that even if that were the case, it would be a good thing because it would mean that my foundations were wrong, so the sooner I start correcting myself, the better. But luckily, it's not been the case, functional programming complements the philosophy I have on software development, and it even helped me classify and understand old concepts in a better way. Many things started falling right into place. other things of course are just dealt with differently, but it gives you an extra point of view, another tool under your belt. And it helps put into relative perspective what you already know, sometimes even realizing that there's extra artificial complexity - the thing I hate the most, we should strive to make things as easy to understand as possible- and this extra complexity is often produced by ultra correctness while following some methodologies. Well, now I know an alternative.

It's always good to learn new things, even if it feels that they are going to contradict what you already know. And here are my key takings, what I've learned so far at the beginning of my journey and how I make sense of it related to everything else I know.

Actions, Calculations and Data

My journey started with this book: https://www.amazon.com/gp/product/B09781TWFL/ref=kinw_myk_ro_title. And it's been a very good first step. This book is kind of a textbook, and it feels is directed to a "junior" audience, or students in a course, and oh I do love it when they make an effort to try to explain things in the easiest way. Even when you are tired after working the whole day, it's easy to follow, the examples and concepts are repeated and reminded on almost every page. Which becomes a little bit annoying when you don't agree on some aspects (there is one small bit that I think it's dangerous without critical thinking. Not to mention that 'Domain Driven Design' is not mentioned until the very last pages when recommending other books. And the piece I disagree with is the piece that DDD would have done better than what's explained in the book).

Anyway, the first part of the book, half of it, it's spent explaining the difference between actions calculation and data: Data is data, a JSON, a constant, a piece of information, an instance of an entity in OOP. Calculations and Actions can also be called pure and impure functions - I will keep calling them calculations and actions in honor of this first book that taught my first steps-. The difference resides that in calculations if you pass the same arguments, you always get the same result, which is not the case with actions. The most common example of an action is reading from a database, it depends on the point in time that this action is executed, whether you get a result or another.

Calculations can be unit tested, which is cool. So most of the effort while being a 'functional programmer' is to convert actions into calculations. To bring what's easily testable together and separate it from what's hard to test, the actions. And make the actions as thin as possible.

If that seems familiar to what you are doing already you wouldn't be wrong, because this thinking and way of doing leads clearly to an architecture pattern that you should be familiar with and it was a pleasant discovery for me:

Onion architecture comes from functional thinking

Well, I am not sure that the creator of the onion architecture was a functional programmer fan -probably not-, but it's an architecture that naturally arises when following functional programming thinking.

I normally refer to the onion architecture as hexagonal architecture, and it can also be called clean architecture. If you wondered what are the differences, you can read this very short article: https://www.thoughtworks.com/insights/blog/architecture/demystify-software-architecture-patterns which has very nice diagrams of each type of architecture.

At grosso modo, they are all the same, the key takings on that are the "What do architectural patterns teach us?" section of the aforementioned article. And they all teach the same.

From now on I will refer to this architecture as either onion or clean because hexagonal is a very long word, and it looks too simplistic in the diagrams above. I think there was no need to create a whole new concept of architecture just because the layers weren't named to the liking, especially since it advocated for the exact same as the others... I like "use cases" more than "object services" - they express better the user execution of a rich domain. But I dislike the inner layer naming "entities" or "object model". I would prefer it to be called 'business domain', and in OOP should include all the tactical patterns of DDD, with an effort to good modeling, and those aren't just entities, but aggregate roots and value objects among others.

It's important to be loose on this aspect. I think this kind of classification is a little bit harmful because it allows zealots to invalidate perfectly valid solutions that can even mix and match all three kinds of architecture, keeping intact the key takings, just because they don't rigidly follow one kind or another.

Going back to the subject at hand: the effort of separating calculation from action is the same as making the inner circle of the architecture, the domain, a calculation, and the actions are at the outer layers. That means that the domain, the center, is completely unit-testable, and it should be. Conversations with domain experts can easily become unit tests when sharing the same ubiquitous language. DDD and functional programming can coexist.

For a specific example, how does a very simple command handler that in PHP looks like this:

final class RegisterUserCommandHandler
{
    public function __construct(
        private readonly UserRepositoryInterface $userRepository
    ) {
    }

    public function __invoke(RegisterUserCommand $command): void
    {
        $user = new User($command->id, $command->email);
        $this->userRepository->add($user);
    }
}

looks with functional programming, if you don't have objects?

Constructor dependencies become explicit dependencies of the function. (Does it feel more explicit to declare the dependencies this way?). So for example, the same in rust, which doesn't encourage the use of objects, will look like this:

pub async fn register_user_command_handler(
    user_repository: &impl UserRepository,
    command: RegisterUserCommand
) -> Result<(), UserDomainError> {
    let user = User::new(command.id, command.email);
    user_repository.add(user);
}

We have this piece of code 'floating' somewhere (from an OOP perspective, that's what it looks like) in a file called "reguster_user_command_handler.rs" or something like that. Pretty much the same thing, don't you think?

Is this a calculation or an action? If the functions make use of actions inside, then it's an action too (hence the effort to put the calculations out by using these patterns like inverse dependency injection). Are we using an action inside? Yes, the saving to the database .add(user).

What if we pass a mock that always returns the same, the mock is actually a calculation, so then this function becomes a calculation, right? Well, is the code in production going to use a mock of the repository, or the real one? In production, this is going to be definitely an action. We can unit-test this handler by passing a mock or a different kind of repo -like in memory- that are calculations. But... how useful is this test going to be? The other line, creation of the user, is already tested as a unit test (it should be, and the test should be clear about how are we expecting the users to be created), so the only thing left to test is that the user is being saved in the database. Does it make sense to test that the user is being saved in an in-memory/mock repo when in production we are going to use a real database? Not much. The only thing left to unit test, at least in PHP it can be done, is to test that the function "add" is going to be executed. That's the only thing left, and it gives little value. Integration tests with an actual database -production alike- will give more value, but they are also more expensive.

sorry I went tangential again, let's refocus on my next key taking about functional programming:

Function as first-class citizens and the decorator pattern.

"Functions as first-class citizens" is what everybody thinks functional programming is. Making functions "important" (honestly, to me, it feels the other way around, variables and stuff are inside functions, so, of course, they are smaller or less important than functions that englobe them, but, apparently, I was wrong). Anyway, this all means that you can pass functions as parameters to other functions.

This, right now, is not that rare, on javascript is quite a common practice - as well as other functional programming practices like the hate for loops, replacing them for functions such as .each() or .map() or .reduce() , because loops are very hard to understand, and a train connection of functions piped all together is not. Despite my sarcasm, if done properly, it's actually easier to understand, easier to separate concerns and keep each function dealing with only one thing and therefore easier to maintain. Stress on 'if done properly'.

Passing a function to be executed below the pipeline is also something commonly used in OOP. An example used in the books is to have a function that logs errors to some specific place. We have a specific function for that, looks like this:

function withLogging(f) {
  try {
    f();
  } catch(error) {
    logSomewhere(error);
  }
}
// then you call it like this:
withLogging(function(){whateverFunctionYouhave()});
// note you have to wrap it in a function or else it we called right away

That specific example is basically how the implementation of loggers for "Buses" (Command Bus, Event Bus, etc) works. They wrap the actual execution of the final function and "decorate" it with extra functionality, in this case, logging. Could either be typical command bus stuff like logging or ensuring transaction atomicity, or whatever thing you can think of.

It's a great way to follow the Single Responsibility Principle of SOLID. Each function does only one thing and does it well. And even to maintain the Open-Closed principle of SOLID. Entities are open for extension and closed for modification. Ok this one is loose, but it's not expected that you start modifying the wrappers, instead is expected that you extend it by adding more and more wrappers.

Wikipedia says SOLID principles are for OOP, but they all match very nicely with functional programming thinking. (The other principles, Liskov substitution, interface segregation and dependency inversion, could be taken at the architecture level, and we've seen already that clean/onion architecture matches perfectly with functional thinking). It's almost as if functional programming does a better job of keeping this principles than OOP.

Immutability is king

Another thing I like about functional programming is the effort made on making everything immutable. It's becoming obvious that mutability is the enemy, even in OOP. Many latest versions of languages start requiring you to declare whether the variable is mutable or not. Something that newest languages like Rust already required from the first day.

Before, when we were speaking about calculations, something that mutates a value received, is not a calculation, a new value must be created from the original, and this is the value to be returned if you want it to be called "pure". There are many techniques like copy-on-write or defensive copying in order to achieve that. It differs greatly for each programming language.

But I am not interested in the low-level aspects of that (probably just by choosing rust you are covered), like copying arrays or such. The interesting thing about that is the application of keeping immutability at the service level, which brings us to another practice commonly used in the DDD world: Event Sourcing.

At a service level, the state of the service is the biggest mutable blob you have. How to make it immutable? Instead of saving the state, saving each action that happened - aka domain events. The state therefore can be regenerated by replaying the actions.

Quoting this book: https://www.amazon.com/gp/product/B09781TWFL/ref=kinw_myk_ro_title

[..] It means that we can build the shopping cart at any time just by recording what items the user adds. We don't have to maintain the shopping cart object all the time. We can regenerate it any time we want from the log.

It's an important technique in functional programming. Imagine recording the items the user adds to the cart in an array. How do you implement undo? You just pop the last item out of the array. We won't get it in this book. Look up event sourcing for a lot more information.

Functional programming and DDD

Having read the first book, I was happy that it enforced most of my previous knowledge, still, it was quite surprising how the whole book dodged the whole DDD world, it even felt forced at some points. Especially since it kind of complement each other, as seen just before, at least in the general aspects.

So I started with the following book: https://www.amazon.com/gp/product/B07B44BPFB/ref=kinw_myk_ro_title (yes F#, first time I've heard of this programming language)

This book finally emphasizes the whole strategic patterns of DDD: all the patterns at a high level, what helps you divide a system into microservices, and bounded contexts and finally it speaks about ubiquitous language (what I think it's the main flaw in the other book, especially since it's directed to juniors, encouraging them to start working with complete disregard to the domain experts - also the separation of departments of the hypothetical company is weird).

So at a high level, it all fits. It's a different story when starting to implement a specific service. It can be shocking. I mean, of course it's going to be different if in functional programming there are no objects - those hated aberrations. Let's just have an open mind.

Modeling the domain is still modeling the domain.

Modeling is still the key part of DDD when taking a functional programming point of view. Modeling is the key aspect of software development in my opinion. With functional programming, we might not have objects, but we do have "data", types, structures, and that's where we are going to put our effort into modeling. Speaking with domain experts and the whole thing. This isn't different from before.

And honestly, if you think you are safe from objects here, you are probably wrong. You can bypass as much as you want them, but it's a huge advantage if you've worked with them before to make proper domains.

For instance, we can, and should, model value objects. Even for the most simple types, value objects give, ehem, value, to your code since it's one step closer to having the domain experts, that don't know programming, understand the coded domain. They can understand "UserEmail" easily, but might get confused by "string" or "int". One step closer to making your code the documentation.

With the OOP style, an example of a value object might look like this:

final class UserEmail{
    private function __construct(
        public readonly string $value
    ){
    }
    public static function fromValue(string $value):self
    {
        if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidValue($value . "not a valid email");
        }
        return new self($value);
    }
}

Value Objects are immutable (something that fits nicely with FP) and we make the constructor private so the only way to create a this value object is through the named constructor fromValue , therefore we are always sure that whenever we have this object, it's a valid one.

The FP version looks, if not the same, very similar, for example, in Rust:

struct UserEmail {
    value: String,
}

pub fn fromValue(value: &String) -> Result<UserEmail, UserDomainError>
{
    if (/* some condition here */) {
        return Err(UserDomainError{});
    }
   return Ok(UserEmail{
       value: value.clone() // cloning, to avoid depending on the original string and keeping immutability. The Rust system of borrowing might just prvent that. I'll know in some months...
   });
}

All right so the creator is not in the object (we could put it actually in the struct), but still it's in a "module" which just made the value object private, and the only way to create it is via the fromValue function. That just looks like the example above with OOP.

Keeping with modeling, we might not have objects in FP, but we have an "algebraic type system" (structs, enums, etc), and that's what we are going to use to compose our entities, value objects as seen above, or aggregate roots. The differences between OOP:

final class User {
    private srtring

and FP:

struct User{
   email: UserEmail
}

are almost nonexistent.

The real differences in modeling

The most important difference in modeling the domain is that in FP you also model the inputs and outputs. The outputs in some FP languages are mandatory, for example, it's very common to return a Result enum in Rust, and the next logical step is to model all the possible errors in the err variant. (A complete example here). The book uses F# which is surprisingly similar to Rust, at least they share the same algebraic type system.

Modeling the inputs is not that different though, do you ever use commands? That's an input type. Just that going "type-based development" makes you wonder whether commands actually belong to the domain layer since they are the entrypoint to execute the domain. I don't have a clear answer to that. But I tend to think that they belong to the application layer. It all boils down to the workflows/command handlers. On OOP it's so clear that they separate the actions from the calculations that I find it adequate to place them in the middle layer. On FP modeling, the workflows, even though you model them as part of the domain, they do the same. Actually, it's also said that the domain expert should see a command handler and understand it, like the workflow.

The trick in any case is to try to open the domain the minimum so the only way it can be executed is through what the workflow/command handler needs. In OOP you would execute the domain from within the object. In FP, there will be a function to do just that. In that sense, the execution of workflows looks way easier and simpler with FP. With OOP you need to make sure that you are executing the domain from within the proper aggregate - an advantage of that is that it helps you model everything better. FP is looser, you don't need to think about encapsulating the function inside an object and not having this constraint might feel liberating.

The downside is, if objects are not "rich" and don't have a behavior attached, then they must be anemic right? Well, there's this danger also in OOP. The trick is the same: keep access to the domain as restricted as possible, and only allow what's needed from the workflow/handler. For FP, the use of modular visibility will help you avoid direct access and wrong usage of the types - as shown above with the value object example. And you can argue it works better/it's safer than typical OOP visibility from OOP languages. With modular visibility, you may just give access to the functions and not to any piece of the domain even!

One indication that you might have an anemic domain is the overuse of setters. That's impossible in FP, since there are no objects, and even further, everything is immutable, so everything -entities and all the types- is treated as if it was a value object, -you never modify it, you create a new one, from a previous one if needed-.

Setters are impossible in FP. And that's good. I always try to avoid them, they are the first step to the road of a CRUD and anemic domain, once you go down that road, you might as well use an Excel spreadsheet. Just that sometimes they give you some useful shortcuts. Imagine an entity with 20 fields (probably a badly designed entity), if you want to modify a single attribute, you will need to write a lot of code just for that. Ok, that also depends on the programming language, so well, setters are just bad. And with FP they are easier to avoid.

The book warns against "class-driven modeling", in the same way it warns against "database-driven modeling". It gives the example of class inheritance, having an abstract BlogPost and then something like final class Published and final class Draft. It states that a domain expert doesn't understand what abstract means. That the domain code should be the documentation.

Of course, if you are in FP that might be the approach, but I would say that the documentation of the code should be the test. The test explains how the domain should be executed. In OOP since there are so many patterns, we might need to hide some aspects from the non-coders, but the tests should always be self-explanatory.

An example where you might want to hide some aspects is if you implement the state pattern, you might have a class for each state, all only accessible through the main aggregate root -on the state pattern, this is called context -, and all of those states might inherit from a class or implement an interface, so you might not want to show the interface, even though it might help for a better discussion on how to model the domain.

I have picked the state pattern on purpose since it's also explained in the book, but not like a pattern, but as the way to model things (honestly, I keep using this pattern more and more... my head naturally tends to FP point of view...). In FP, we just have different types stating each state, easy peasy, it's how I would like the OOP to work (if you don't know how the pattern works, you can easily get things wrong and execute the domain in a non-intended way). Again this is impossible in FP, just that with OOP it gives shortcuts that I think in FP are harder to achieve, like having to code the context (the main aggregate root) just once. On FP you will need to declare all the states by creating each type, and they might look very similar to each other. It can get interesting though, and since you are just working with types, without having to program anything else yet while modeling, it's going to be easy to change those types and have dynamic conversations with the domain experts.

Finally, the thing I disagree with the approach explained in the book on modeling the domain is the complete disregard for UML diagrams. It also says, "like class driven modeling" that a domain expert won't understand them (I can wholeheartedly agree- I had some frustrating experiences). But disregarding them all together is non-advisory in my opinion. You might want not to share it, but trying to make a UML will help greatly with the design, whether you are designing objects or types.

Another difference

One of the first differences is this one: "Avoid domain events within a bounded context".

First of all, if a different bounded context than the publisher of the domain event subscribes to that event, the event can be called an integration event. You need infrastructure support for that, Kafka, event bridge, SNS, simple calls, you name it.

Now, Domain Events in the same bounded context might not need infrastructure. You can have listeners that synchronously consume the event being published, and you avoid the hustle of relying on infrastructure.

The book advocates against that, saying that it puts hidden dependencies and it's harder to follow. I agree. Why is that such a common practice in OOP DDD? Being so "uncomfortable" as the book says? Because of this rule about aggregates:

Rule: Model True Invariants in Consistency Boundaries

Quote from https://www.amazon.com/gp/product/B00BCLEBN8/ref=kinw_myk_ro_title. Ok, this rule is hard to understand, more down into plain words:

Thus, an aggregate is synonymous with a transactional consistency boundary [...]. A properly designed Aggregate is one that can be modified in any way required by the business with its invariants completely consistent within a single transaction. And a properly designed Bounded Context modifies only one Aggregate instance per transaction in all cases.

To keep with this rule, in the case of having to modify two aggregate roots on a single action, domain event listeners are being used, separating the action into many transactions as needed. This way keeps aggregates independent, decoupled, modular and maintainable without side effects on other aggregates. It's a rule of thumb for designing proper aggregate roots.

Domain events listeners inside the same bounded context can be made asynchronous. Like integration events between bounded contexts, you will need some kind of infrastructure to support that (the most minimalistic approach would be with queues programmed in the same programing language that you use with threads, good luck with that.) With this approach suddenly each event listener becomes a service on its own and could be deployed independently, the bounded context boundaries will start to blur. You can bring this approach to the extreme, and you will have a microservices explosion, a bunch of deployable units, the most decoupled system. Just try to maintain a cool map of publishers/subscribers for your sanity.

That's basically the approach that, if not explicitly explained, it's implicitly insinuated. It doesn't go as deep as to explain bounded contexts, that's why it's left unanswered.

What's easier

Not having objects eliminates instantly some problems that are often quite complicated to get right with OOP. I was explaining before about the aggregate rules. Those rules are found in the red book. Have small aggregates, reference aggregates always by ID, never with the full object, and only one transaction per aggregate root. If you see, the last two rules are basically restricting object navigation. You can't navigate form one aggregate root to another aggregate root (you should get it from the repository). Navigations are restricted within the aggregate root, and that, in 99% of the cases means one step. Those rules are basically to restrict navigation as much as possible. To restrict relationships. Because relationships are difficult. And what happens if you don't have actual objects? You don't have relationships! Modeling is suddenly also easier, (at least modeling commands, not sure if you would like to have some tables listing stuff, the book didn't really got into the "query" side).

And did I say repositories? So no such pattern either, you don't have a collection of entities - those object-look-alike despicable things. While persistence is also needed, it's basically in the form of primitives -of course-, converting the types to primitives to be saved to any persistence mechanism, well, nothing new under the sun here.

What's more complicated

What's more complicated is the attempt to make every workflow in a single "pipeline" joining the inputs and outputs of each function together, dealing with errors and making those inputs and outputs match the types so the compiler accepts them. You get a little bit of this if you use often the Result enum in Rust, or similar. It forces you to think about error handling and to decide some things early in the development cycle. That's being sold as one of the advantages, but sometimes is too early to know if it's better to panic! or to treat the exception.

This whole matching input/output types thing could be avoided altogether - honestly- by just not trying so hard to use a single pipeline and have some code in between, but I guess there's some pleasure in having a workflow that does one thing, and it can be extended, but hardly modified. I guess you go down this path you become more and more obsessed with the correctness and elegance of your code.

Final words

FP is after all another set of tools that you have under your belt. Following FP kind of forces you to follow good practices so I would encourage the use of FP in all aspects. Applying good practices in OOP makes the code look more FP, the standards of the industry kind of converge there, getting rid of the extra complexity that OOP brings and using the best that each world has to offer.

I wish I had known -or read the books- before, so I would have applied some principles about immutability and I would understand better how to properly use Rust as a programming language in my other series: https://blog.cesc.cool/series/develop-with-me. Still, I wasn't that out of track, I am just adding some extra complexity in the domain in my stubbornness to keep using an OOP style with Rust, but the architecture and some piping and other kinds of functional thinking is already there.

Thanks for reaching the end of this very long article, I hope you enjoyed it and learned some things along the way.

This article has been written by chat GPT