Thomas Buss blog

Reinventing Objects

Table of Contents

Since I learned about Gary Bernhardt’s concept of Functional Core, Imperative Shell, I have been using “Imperative Coordinators” in most of my code. An imperative coordinator is a simple piece of code that itself does not contain any logic, but calls other components for calculations, decision making and performing side effects. The emphasis here is on the word simple. An imperative coordinator should not contain any logic besides making an early return if some of the previous components signaled an error somehow, let that be an exception or a result object.

I find this approach a lot easier to understand than the ballet of intertwined objects that is present in some codebases I have encountered. It combines the ease of a scripting-approach with the testability of pure functions, as the majority of code resides in stateless functions where testing is a breeze. Because the imperative coordinator only delegates tasks and contains no significant logic, there is almost no need to test it.

However, pure functions are not the only kind of code the imperative coordinator calls. To make any program useful, side effects are required.

In the book “Grokking Simplicity”, the author Eric Normand emphasizes that you should clearly separate data, calculations and actions. Data is probably the simplest to understand: anything you load from a database, file or external system in general, including incoming requests and so forth. Calculations are another name for pure functions: these functions only produce an output and do nothing else. Actions actually do side effects and therefore make the program useful. In functional programming, you try to maximize the amount of code in calculation because these functions compose well and are easy to reason about. This is not to say that functional programmers are afraid of side effects. Rather, the actions where side effects can happen are called explicitly; as explicit is the handling of potential errors.

This is somewhat contrary to OOP where the actions an object performs are deliberately hidden from the client. When you call getProduct on an object, you’re telling it: Get me that product data, whatever it takes! In functional programming, on the other hand, we would very much like to know if a call performs a side effect, because this is crucial to the way functional programmers think about the parallelization, caching, and error handling.

This insight has led me to a train of thought I want to share with you today. I call it the Reinvention of Objects.

Starting with functions

The most basic building block for any useful program is a pure function. Pure functions are awesome! They are incredibly easy for the programmer to write and test, and are likewise easy for the compiler to optimize. Caching the result of a pure function is trivial and parallelizing pure functions is a no-brainer. Imagine you are building a web service that converts from one unit of measure to another. Given an easy-to-use web framework, you can already build a useful service only with pure functions, as our service for unit conversion just takes an input request and produces an output response.

In real-world examples, however, pure functions are not going to be sufficient. For example, if we want to convert US dollars to Euros, we need to get the current exchange rate from somewhere. Let’s say there is an API that we can use to get the value. Now, our unit conversion code changes from a pure function to a side effect conducting action because performing an HTTP request is a side effect. We now need to deal with the possibility of a network error, but this shouldn’t be too hard as long as the program logic is still simple. Adding some error mapping or default value should cover it.

Injecting parameters

Imagine that the API used to get the current exchange rate requires an API key to be sent along. We do not want to store the key in the source code, so it needs to be read from somewhere like an environment variable or a config file when the service starts. Again, we introduce the possibility of errors, but we also have another problem: How does the API key, once it is loaded into memory by whatever mechanism we chose, end up as a parameter to our business code? We could pass the key around from the main function down to whatever function needs it, but this might be tedious. Moreover, we do not want our business code to know that there even is such a thing as an API key. It is a technical detail that we’d prefer to keep out of sight unless we actually care about it.

The functional way to do this is to use currying and partial application to inject the key into the function that makes the API call. This means that the number of parameters of the original function would be reduced by one. Surrounding code would not know anything about API keys, only units of measurements and so on, as it should.

The OOP way to deal with this problem is to create an object that, when constructed, receives the API key as a parameter and saves it in a private field. When performing the API calls, the object can then inject the key into the request without requiring the client to supply the key when invoking the method. As with the functional approach, the client code does not know about API keys, as it should.

Local state enters the scene

So far, we have assumed that the API key is valid all the time. But there might be scenarios where this is not the case. Imagine that we have credentials that can be used to obtain a temporal API token that is only valid for an hour or so. Our API-calling logic would need to first get the token, store it, and use it for any request that occur within the validity time of the token. Once the token is expired, a new token has to be acquired.

The functional approach of currying and partial application does not work anymore. Personally, I think this is where purely functional languages like Haskell lose most developers. Many developers feel like it becomes unreasonably hard to deal with Monadic data structures that abstracts this state management for something that is so simple in imperative code. Some languages, like OCaml or F#, take a practical approach here and offer an escape hatch out of the functional world by allowing mutable variables that behave just like in imperative languages most programmers are familiar with.

For the OOP approach, the facade that the clients see from the outside does not change and the code inside the object is still fairly simple. It should also be fairly easy to switch from an object to an actor: For example, let’s say we want to refresh the token as soon as it expires. An object can only make the necessary API call when it is invoked by an outside client. On the other hand, an actor can make this decision without any interaction, as it is an independently running process. In a way, both objects and actors act like a code representation of an external system that other parts of the program can use. If we started with objects to begin with, all other changes to the API interaction we introduced along the way (except the error handling) would not have affected the client code.

So objects are better?

Earlier, I have stated that functions, especially pure functions, are awesome and then provided an example where their simplicity breaks apart. However, do not think that I take OOP as the superior paradigm. I believe there is a nuance to find here.

Objects and actors make a lot of sense when you want to interact with the outside world, as it always involves some form of localized state (e.g. tokens, connections). This might be an API token, or a file pointer, a cache or some sensor value. Alan Kay’s original idea of OOP, where objects are like little servers that we can send and receive messages with, encompasses this perfectly. Objects are also great for some internal components of a program, like a database connection pool or a request router.

However, the complexity from objects increases when we try to model everything as an object and when even simple tasks require the composition of multiple objects that have nothing to do with other parts of the program that justify some sort of coordination. I do not think that we should create objects for sorting lists. Sorting should be implemented as a pure function, as it does not involve any kind of localized state. I also do not think that a customer’s shipping address is an object in the sense that Alan Kay coined the term. An address is just data, a bunch of key/value pairs that we pass around in the program.

What I do think is that most programs should only consist of a handful of objects or actors, most (if not all) of them singletons created at application startup and available globally to serve the actual requests that are handled in imperative coordinators. Individual use-case implementations you typically find in web servers, like “add item to shopping cart” or “mark TODO as complete” can best be implemented as imperative coordinators that receive data, transform and make decisions with it using pure functions as the containers of business logic and reach out to objects or actors to do something useful before returning some other data back to the client. CLI tools are often even simpler, as there is no need to spin up objects like little servers if they will be shut down immediately after one use.

Bonus question: So anemic data is better?

To some readers it might sound like I am advocating for anemic data models instead of rich domain models since I have stated that “An address is just data, a bunch of key/value pairs”. However, this was not my intention. For me, the two decisions are orthogonal to each other. I might use simple data types, but I might as well embed business logic into objects (objects here referring not to the Alan Kay kind, but the Java/C#/C++ kind), so that only valid state transitions are allowed. Coming back to Gary Bernhardt’s concept of “Functional Core, Imperative Shell”, having immutable value types with associated functions that do not modify the state of an object, but rather return a new copy of that object, is something that I find quite appealing.

Conclusion

We’ve looked at how functional programming and OOP paradigms approach a common problem and have seen that pure functional programming can become cumbersome when requirements change. We have also seen that OOP might provide a better facade to clients, but involves a different set of problems that need to be tackled.

Hopefully you found this blog post helpful. It reflects my current interpretation of functional programming and OOP and where they fall short. I have been thinking about this for quite a while now and hope my reasoning from this blog post can help you evaluate your use of functional programming and OOP in your own projects. Feel free to comment if you have a similar or different opinion or would like to know more.


EDIT 2024-12-25: Elaborated more in the introduction and removed the Java paragraph as it created more confusion than insight. Also added a bonus paragraph to (hopefully) clear things up even more. Minor other adjustments.

Comments powered by Disqus